{"id":"l-rn-3-4","courseId":"react-native-apps","lang":"ar","kind":"lesson","summary":"ابنِ نموذج إضافة رحلة في Trailhead بحيث يعمل مع لوحة مفاتيح الهاتف، فينتقل بين الحقول، ويبقى ظاهرًا فوق لوحة المفاتيح، ويحفظ من النقرة الأولى، ويتحقّق من المدخلات برسائل خطأ واضحة لكل حقل.","html":"<p>النماذج هي المكان الذي تختلف فيه تطبيقات الجوّال عن الويب أكثر من أي مكان آخر. لوحة المفاتيح تغطّي نصف الشاشة، ولا يوجد مفتاح Tab، والنقرة الأولى على زرّ أحيانًا لا تفعل إلا إغلاق لوحة المفاتيح، والكتابة على الزجاج بطيئة لدرجة أن كل رسالة خطأ غير ضرورية تبدو شخصية. لنموذج “Log a hike” في Trailhead خمسة حقول: الاسم، والتاريخ، والمسافة، والمدّة، والملاحظات. وهكذا تجعله مريحًا.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">مسوّدة من النصوص الخام</h2>\n<p>احفظ كل ما كتبه المستخدم في كائن واحد من <strong>النصوص</strong>، كما كُتب بالضبط. وحلّله حين تحتاج إلى أرقام، كما فعلت مع <code dir=\"ltr\">parseDistanceKm</code> في القسم 1:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">src/app/new-hike.tsx</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\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { useRef, useState, </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ComponentRef } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'react'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { TextInput } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'react-native'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Draft</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">date</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">distance</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">duration</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">notes</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// Today's date in the phone's time zone. toISOString() would give the UTC date,</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// which is already tomorrow (or still yesterday) for part of the day in most places.</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> localToday</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\"> now</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> new</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> Date</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:#8250DF;--shiki-dark:#D2A8FF\"> pad</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">n</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> String</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(n).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">padStart</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'0'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">now</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getFullYear</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">()</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}-${</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">pad</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">(</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">now</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getMonth</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">() </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">)</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}-${</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">pad</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">(</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">now</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getDate</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>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> emptyDraft</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ()</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Draft</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ({ name: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">''</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, date: </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">localToday</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(), distance: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">''</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, duration: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">''</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, notes: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">''</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> });</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> default</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> NewHike</span><span style=\"color:#953800;--shiki-dark:#FFA657\">() </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:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">draft</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">setDraft</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useState</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Draft</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>(emptyDraft);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">submitted</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">setSubmitted</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useState</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">false</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:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">touched</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">setTouched</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useState</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Partial</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Record</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">keyof</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Draft</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">boolean</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\"> distanceRef</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useRef</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">ComponentRef</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">typeof</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> TextInput>>(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">null</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\"> durationRef</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useRef</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">ComponentRef</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">typeof</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> TextInput>>(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> set</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">field</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> keyof</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Draft</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">text</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> setDraft</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">d</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ({ </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">...</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">d, [field]: text }));</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">  // …errors, save and JSX below</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>تعطي الدالة المساعدة <code dir=\"ltr\">set('distance')</code> كل حقل <code dir=\"ltr\">onChangeText</code> دون خمس دوال شبه متطابقة. وتخزين النصوص يعني أن “8,” تبقى “8,” أثناء الكتابة، بدلًا من أن تنهار إلى <code dir=\"ltr\">8</code> أو <code dir=\"ltr\">NaN</code>.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">التنقّل بين الحقول</h2>\n<p>لا يوجد مفتاح Tab، فيقوم مفتاح الإدخال بالمهمّة. سمِّه باستخدام <code dir=\"ltr\">returnKeyType=&quot;next&quot;</code>، وحين يُضغط، ركّز على الحقل التالي عبر ref:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">TSX</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\">TextInput</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  value</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">draft.name</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  onChangeText</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">set</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'name'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  placeholder</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Ridge Loop\"</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  autoCapitalize</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"words\"</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  returnKeyType</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"next\"</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  submitBehavior</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"submit\"</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  onSubmitEditing</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> distanceRef.current?.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">focus</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">()</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  onBlur</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> setTouched</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">t</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ({ </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">...</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">t, name: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">true</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }))</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</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\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">TextInput</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  ref</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">distanceRef</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  value</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">draft.distance</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  onChangeText</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">set</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'distance'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  keyboardType</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"decimal-pad\"</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  returnKeyType</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"next\"</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  onSubmitEditing</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> durationRef.current?.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">focus</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">()</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  onBlur</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> setTouched</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">t</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ({ </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">...</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">t, distance: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">true</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }))</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">/></span></span></code></pre></div>\n<p>يُبقي <code dir=\"ltr\">submitBehavior=&quot;submit&quot;</code> لوحة المفاتيح مفتوحة بينما ينتقل التركيز، فلا تومض مغلقةً ثم مفتوحةً من جديد. وفي الحقل الأخير استخدم <code dir=\"ltr\">returnKeyType=&quot;done&quot;</code> ودعه يرسل النموذج. وتنبيه صريح: لوحات المفاتيح الرقمية على iOS ليس فيها مفتاح إدخال أصلًا، فينقر المستخدمون على الحقل التالي أو على زرّ في شريط أدوات. هذا طبيعي، وسبب آخر لعدم الاعتماد على لوحة المفاتيح وحدها.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">إبقاء الحقول فوق لوحة المفاتيح</h2>\n<p>حين تنفتح لوحة المفاتيح، تستقرّ فوق تطبيقك. والحقل القريب من الأسفل (الملاحظات) يختفي خلفها. الإصلاح المعتاد يغلّف النموذج بـ <code dir=\"ltr\">KeyboardAvoidingView</code> ويضع <code dir=\"ltr\">ScrollView</code> بداخله:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">TSX</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\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { KeyboardAvoidingView, Platform, ScrollView } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'react-native'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">KeyboardAvoidingView</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> style</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{ flex: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> behavior</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">Platform.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">OS</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ===</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'ios'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ?</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'padding'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> :</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> undefined</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</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\">ScrollView</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">    contentContainerStyle</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{ padding: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">16</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, gap: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">16</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">    keyboardShouldPersistTaps</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"handled\"</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">    keyboardDismissMode</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"on-drag\"</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\">    {</span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">/* fields and the Save button */</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">ScrollView</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\">KeyboardAvoidingView</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span></code></pre></div>\n<p>على iOS، تضيف <code dir=\"ltr\">padding</code> حشوًا سفليًا يساوي ارتفاع لوحة المفاتيح، فينكمش الـ ScrollView ويمكن تمرير الحقل المركّز عليه إلى منطقة الرؤية. أما على Android فعادةً ما تتغيّر أبعاد النافذة لاستيعاب لوحة المفاتيح أصلًا، لذا فإن <code dir=\"ltr\">undefined</code> هي القيمة الموصى بها. ويغلق <code dir=\"ltr\">keyboardDismissMode=&quot;on-drag&quot;</code> لوحة المفاتيح حين يمرّر المستخدم، وهذا يبدو طبيعيًا.</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>زرّ Save الذي يحتاج إلى نقرتين</span></p>\n<p>افتراضيًا، يعامل الـ ScrollView النقرة خارج الحقل المركّز عليه على أنها «أغلق لوحة المفاتيح» ولا شيء غير ذلك. ينقر المستخدم على Save، فتنغلق لوحة المفاتيح، ولا يُحفظ شيء؛ فينقر مرة أخرى. يسمح <code dir=\"ltr\">keyboardShouldPersistTaps=&quot;handled&quot;</code> للنقرات على الأزرار والعناصر القابلة للمس بالمرور، مع إبقاء إغلاق لوحة المفاتيح عند النقر على مساحة فارغة. ضعه على كل ScrollView أو FlatList يحوي حقول إدخال.</p>\n</aside>\n<p>وللنماذج الطويلة ذات الحقول الكثيرة، توصي وثائق Expo بمكتبة <code dir=\"ltr\">react-native-keyboard-controller</code>، التي يمرّر الـ <code dir=\"ltr\">KeyboardAwareScrollView</code> الخاص بها الحقل المركّز عليه إلى منطقة الرؤية تلقائيًا؛ ويضيف <code dir=\"ltr\">KeyboardToolbar</code> من المكتبة نفسها فوق لوحة المفاتيح أزرارًا للسابق والتالي والإنهاء. تحتاج إلى development build، وهو ما سيحصل عليه Trailhead في القسم 4.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">كلمة عن التواريخ</h2>\n<p>تحتفظ مسوّدة Trailhead بالتاريخ نصًّا بصيغة <code dir=\"ltr\">YYYY-MM-DD</code>، قيمته الافتراضية تاريخ اليوم، لأن معظم الرحلات تُسجَّل في اليوم نفسه. لكن كتابة التواريخ على لوحة مفاتيح الهاتف مزعجة، لذا تعرض تطبيقات الإنتاج منتقي التاريخ الخاص بالمنصّة: عجلة أو تقويم على iOS، ونافذة حوار على Android. تغلّف حزمة المجتمع <code dir=\"ltr\">@react-native-community/datetimepicker</code> الاثنين معًا، وهي مثال جيد على component يختلف سلوكه بين المنصّتين بما يكفي لتريد معه الملفات الخاصة بكل منصّة من القسم 2.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">تحقّق لا يُزعج</h2>\n<p>احفظ القواعد في دالة نقية: المسوّدة تدخل، والأخطاء تخرج. والكائن الفارغ يعني أن المدخلات صالحة.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">src/lib/validateHike.ts</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\">export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> validateHike</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">d</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Draft</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\"> errors</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Partial</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Record</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">keyof</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Draft</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">string</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>> </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {};</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">d.name.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">trim</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">()) errors.name </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'Give the hike a name.'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">parseDistanceKm</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(d.distance) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) errors.distance </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'Enter a distance in km, like 8.4.'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> errors;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>ثم قرّر <strong>متى</strong> تعرض كل رسالة. عرض الأخطاء من أول ضغطة مفتاح يعاقب الناس لأنهم لم ينتهوا من الكتابة. وانتظار نافذة تنبيه بعد Save يُخفي النموذج خلف قائمة يجب أن يحفظوها. النمط الذي ينجح: اعرض خطأ الحقل بعد أن يغادره المستخدم (<code dir=\"ltr\">touched</code>) أو يضغط Save (<code dir=\"ltr\">submitted</code>)، بجوار الحقل، وأبقِه حيًّا من تلك اللحظة فيختفي لحظة إصلاحه.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">TSX</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\"> errors</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> validateHike</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(draft);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> show</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">field</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> keyof</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Draft</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (submitted </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">||</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> touched[field]) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">&#x26;&#x26;</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> errors[field];</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> save</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() {</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  setSubmitted</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">true</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (Object.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">keys</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(errors).</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">length</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ></span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  dispatch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({ type: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'added'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, hike: </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toHike</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(draft) });</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  router.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">back</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>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// under the distance input:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">show</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'distance'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Text</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> style</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">={</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">{ color: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'#b42318'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">{</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">errors.distance</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">}</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">Text</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">> </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>لمستان لإمكانية الوصول تكلّف كلٌّ منهما سطرًا واحدًا: أعطِ كل حقل <code dir=\"ltr\">accessibilityLabel</code> يطابق تسميته المرئية، وأبقِ نص الخطأ تحت حقله مباشرة ليصل إليه قارئ الشاشة بعده.</p>\n<p>تحوّل <code dir=\"ltr\">toHike</code> المسوّدة الصالحة إلى <code dir=\"ltr\">Hike</code> حقيقي (تحلّل الأرقام وتولّد معرّفًا). وبعد الحفظ، يعيد <code dir=\"ltr\">router.back()</code> المستخدم إلى السجلّ، حيث تكون الرحلة الجديدة في الأعلى أصلًا لأن الـ reducer وضعها هناك.</p>\n<p>يفتح النموذج حاليًا شاشةً عادية مُضافة إلى الـ stack. في الدرس التالي ستعرضه بوصفه modal، وتحمي الشاشات خلف تسجيل الدخول، وتسمح للروابط بفتح Trailhead على رحلة محدّدة.</p>\n","toc":[{"id":"sec-1","text":"مسوّدة من النصوص الخام","level":2},{"id":"sec-2","text":"التنقّل بين الحقول","level":2},{"id":"sec-3","text":"إبقاء الحقول فوق لوحة المفاتيح","level":2},{"id":"sec-4","text":"كلمة عن التواريخ","level":2},{"id":"sec-5","text":"تحقّق لا يُزعج","level":2}],"takeaways":["احفظ مسوّدة النموذج نصوصًا خامًا في كائن state واحد، ولا تحلّلها وتتحقّق منها إلا حين تحتاج إلى النتيجة.","اربط الحقول ببعضها باستخدام <code dir=\"ltr\">returnKeyType=&quot;next&quot;</code> والـ refs، فينقلك مفتاح الإدخال إلى الحقل التالي بدلًا من إغلاق لوحة المفاتيح.","غلّف النموذج بـ <code dir=\"ltr\">KeyboardAvoidingView</code> مع <code dir=\"ltr\">behavior=&quot;padding&quot;</code> على iOS، وأعطِ الـ ScrollView الداخلي <code dir=\"ltr\">keyboardShouldPersistTaps=&quot;handled&quot;</code>.","تحقّق من المدخلات في دالة نقية تُرجع كائن أخطاء، واعرض كل خطأ بجوار حقله بعد أول محاولة حفظ أو بعد مغادرة الحقل."],"further":[{"title":"Keyboard handling","url":"https://docs.expo.dev/guides/keyboard-handling/"},{"title":"KeyboardAvoidingView","url":"https://reactnative.dev/docs/keyboardavoidingview"},{"title":"ScrollView keyboardShouldPersistTaps","url":"https://reactnative.dev/docs/scrollview#keyboardshouldpersisttaps"}],"quiz":[{"q":"<p>يقول المستخدمون إنهم مضطرون للنقر على Save مرتين في نموذج Trailhead حين تكون لوحة المفاتيح مفتوحة. النقرة الأولى تغلق لوحة المفاتيح فقط. ما الإصلاح؟</p>","options":[{"text":"استدعِ <code dir=\"ltr\">Keyboard.dismiss()</code> في بداية دالة الحفظ.","why":"دالة الحفظ لم تعمل أصلًا في النقرة الأولى، فالكود داخلها لا يستطيع المساعدة."},{"text":"اضبط <code dir=\"ltr\">keyboardShouldPersistTaps=&quot;handled&quot;</code> على الـ ScrollView الذي يحوي النموذج.","why":"صحيح. افتراضيًا، النقرة خارج الحقل المركّز عليه أثناء ظهور لوحة المفاتيح تغلق لوحة المفاتيح فقط؛ أما <code dir=\"ltr\">handled</code> فتسمح للنقرات على الأزرار بالمرور."},{"text":"انقل زرّ Save فوق الحقول.","why":"النقرة تُبتلع أينما كان الزر، ما دام داخل ذلك الـ ScrollView."}],"answer":1},{"q":"<p>على iOS، يختفي حقل الملاحظات في أسفل النموذج خلف لوحة المفاتيح حين يُركَّز عليه. ما الإعداد المعتاد لإصلاح ذلك؟</p>","options":[{"text":"<code dir=\"ltr\">KeyboardAvoidingView</code> مع <code dir=\"ltr\">behavior=&quot;padding&quot;</code> و<code dir=\"ltr\">flex: 1</code> حول الـ ScrollView الخاص بالنموذج.","why":"صحيح. يضيف الـ view حشوًا سفليًا يساوي ارتفاع لوحة المفاتيح، فينكمش الـ ScrollView ويستطيع تمرير الحقل إلى منطقة الرؤية."},{"text":"<code dir=\"ltr\">position: 'absolute'</code> على حقل الملاحظات مع <code dir=\"ltr\">zIndex</code> مرتفع.","why":"لوحة المفاتيح نافذة نظام فوق تطبيقك؛ ولا يوجد zIndex يضع الـ view الخاص بك فوقها."},{"text":"اضبط <code dir=\"ltr\">autoFocus</code> على حقل الملاحظات.","why":"هذا يفتح لوحة المفاتيح عند التركيب، فيحدث التداخل أبكر، ولا يختفي."}],"answer":0},{"q":"<p>متى يجب أن يعرض Trailhead الخطأ “Distance must be a number” تحت حقل المسافة؟</p>","options":[{"text":"مع كل ضغطة مفتاح منذ الحرف الأول.","why":"كتابة “8,” في الطريق إلى “8,4” غير صالحة للحظة. والأخطاء أثناء الكتابة مجرّد ضجيج."},{"text":"في نافذة تنبيه فقط بعد Save، تسرد كل الأخطاء دفعة واحدة.","why":"نافذة التنبيه تُخفي النموذج، ويضطر المستخدم إلى تذكّر القائمة بعد إغلاقها."},{"text":"بعد أن يغادر المستخدم الحقل أو يضغط Save، بجوار الحقل، مع تحديثه حيًّا بعد ظهوره.","why":"صحيح. انتظر حتى ينتهي المستخدم من الحقل، ثم أبقِ الرسالة متزامنة بينما يصلحها."}],"answer":2},{"q":"<p>كيف تجعل مفتاح الإدخال في حقل الاسم ينقل التركيز إلى حقل المسافة؟</p>","options":[{"text":"<code dir=\"ltr\">tabIndex={2}</code> على حقل المسافة.","why":"هذه سمة ويب. لوحات مفاتيح الهواتف ليس فيها ترتيب Tab يمكن ضبطه."},{"text":"<code dir=\"ltr\">returnKeyType=&quot;next&quot;</code> على حقل الاسم و<code dir=\"ltr\">autoFocus</code> على حقل المسافة.","why":"<code dir=\"ltr\">autoFocus</code> يعمل مرة واحدة عند التركيب، لا حين يُضغط مفتاح الإدخال في حقل الاسم."},{"text":"لا شيء؛ لوحات مفاتيح الجوّال تنتقل إلى الحقل التالي تلقائيًا.","why":"لا تفعل. دون معالجة ذلك، يغلق مفتاح الإدخال لوحة المفاتيح في الحقل ذي السطر الواحد."},{"text":"<code dir=\"ltr\">returnKeyType=&quot;next&quot;</code> مع <code dir=\"ltr\">onSubmitEditing={() =&gt; distanceRef.current?.focus()}</code>، وref على حقل المسافة.","why":"صحيح. تسمية المفتاح تقول Next، والمعالج ينقل التركيز."}],"answer":3}],"exercise":{"kind":"web","mode":"js","starter":{"js":"// Return an object with one message per invalid field. {} means the draft is valid.\n// `today` is the current date as 'YYYY-MM-DD', passed in so the function stays pure.\nfunction validateHikeForm(draft, today) {\n  const errors = {};\n  if (draft.name === '') errors.name = 'Give the hike a name.';\n  return errors;\n}\n\nconst draft = { name: 'Ridge Loop', date: '2026-10-04', distance: '8,4', duration: '', notes: '' };\nconsole.log(validateHikeForm(draft, '2026-10-05'));\n"},"solution":{"js":"function isRealDate(text) {\n  if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(text)) return false;\n  const [y, m, d] = text.split('-').map(Number);\n  const date = new Date(Date.UTC(y, m - 1, d));\n  return date.getUTCFullYear() === y && date.getUTCMonth() === m - 1 && date.getUTCDate() === d;\n}\n\nfunction validateHikeForm(draft, today) {\n  const errors = {};\n  const name = draft.name.trim();\n  if (!name) errors.name = 'Give the hike a name.';\n  else if (name.length > 60) errors.name = 'Keep the name under 60 characters.';\n\n  if (!isRealDate(draft.date)) errors.date = 'Use a real date like 2026-10-04.';\n  else if (draft.date > today) errors.date = \"That date hasn't happened yet.\";\n\n  const cleaned = draft.distance.trim().replace(',', '.');\n  const km = Number(cleaned);\n  if (!/^\\d+(\\.\\d+)?$/.test(cleaned) || km <= 0 || km > 200) errors.distance = 'Enter a distance in km, like 8.4.';\n\n  const duration = draft.duration.trim();\n  if (duration && (!/^\\d+$/.test(duration) || Number(duration) < 1 || Number(duration) > 1440)) {\n    errors.duration = 'Enter whole minutes between 1 and 1440.';\n  }\n\n  if (draft.notes.length > 500) errors.notes = 'Notes are limited to 500 characters.';\n  return errors;\n}\n\nconst draft = { name: 'Ridge Loop', date: '2026-10-04', distance: '8,4', duration: '', notes: '' };\nconsole.log(validateHikeForm(draft, '2026-10-05'));\n"},"checks":[{"id":"valid-draft","test":"const ok = { name: 'Ridge Loop', date: '2026-10-04', distance: '8,4', duration: '', notes: '' }; const T = '2026-10-05'; const keys = (d) => Object.keys(validateHikeForm({ ...ok, ...d }, T)).sort().join(); return typeof validateHikeForm === 'function' && keys({}) === '' && keys({ duration: '135', notes: 'Windy.' }) === '';"},{"id":"name","test":"const ok = { name: 'Ridge Loop', date: '2026-10-04', distance: '8,4', duration: '', notes: '' }; const T = '2026-10-05'; const keys = (d) => Object.keys(validateHikeForm({ ...ok, ...d }, T)).sort().join(); return keys({ name: '   ' }) === 'name' && keys({ name: 'x'.repeat(61) }) === 'name';"},{"id":"date","test":"const ok = { name: 'Ridge Loop', date: '2026-10-04', distance: '8,4', duration: '', notes: '' }; const T = '2026-10-05'; const keys = (d) => Object.keys(validateHikeForm({ ...ok, ...d }, T)).sort().join(); return keys({ date: '04/10/2026' }) === 'date' && keys({ date: '2026-02-30' }) === 'date' && keys({ date: '2026-10-06' }) === 'date' && keys({ date: '2026-10-05' }) === '';"},{"id":"distance","test":"const ok = { name: 'Ridge Loop', date: '2026-10-04', distance: '8,4', duration: '', notes: '' }; const T = '2026-10-05'; const keys = (d) => Object.keys(validateHikeForm({ ...ok, ...d }, T)).sort().join(); return keys({ distance: '' }) === 'distance' && keys({ distance: 'far' }) === 'distance' && keys({ distance: '0' }) === 'distance' && keys({ distance: '12.5' }) === '';"},{"id":"duration","test":"const ok = { name: 'Ridge Loop', date: '2026-10-04', distance: '8,4', duration: '', notes: '' }; const T = '2026-10-05'; const keys = (d) => Object.keys(validateHikeForm({ ...ok, ...d }, T)).sort().join(); return keys({ duration: '2h' }) === 'duration' && keys({ duration: '0' }) === 'duration' && keys({ duration: '1441' }) === 'duration';"},{"id":"several","test":"const ok = { name: 'Ridge Loop', date: '2026-10-04', distance: '8,4', duration: '', notes: '' }; const T = '2026-10-05'; const keys = (d) => Object.keys(validateHikeForm({ ...ok, ...d }, T)).sort().join(); return keys({ name: '', distance: 'x', notes: 'n'.repeat(501) }) === 'distance,name,notes';"}],"title":"تحقّق من نموذج إضافة رحلة","prompt":"<p>اكتب <code dir=\"ltr\">validateHikeForm(draft, today)</code> لنموذج Trailhead. يحمل <code dir=\"ltr\">draft</code> النصوص الخام التي كتبها المستخدم: <code dir=\"ltr\">name</code> و<code dir=\"ltr\">date</code> و<code dir=\"ltr\">distance</code> و<code dir=\"ltr\">duration</code> و<code dir=\"ltr\">notes</code>. و<code dir=\"ltr\">today</code> هو تاريخ اليوم بصيغة <code dir=\"ltr\">YYYY-MM-DD</code>. أرجع كائنًا فيه رسالة لـ <strong>كل حقل غير صالح</strong> (أي نصّ غير فارغ)؛ وأرجع <code dir=\"ltr\">{}</code> حين يكون كل شيء صالحًا.</p>\n<ul>\n<li><strong>name</strong>: مطلوب بعد حذف المسافات الطرفية؛ و60 حرفًا على الأكثر.</li>\n<li><strong>date</strong>: بصيغة <code dir=\"ltr\">YYYY-MM-DD</code>، وتاريخ موجود فعلًا (<code dir=\"ltr\">2026-02-30</code> غير موجود)، وليس بعد <code dir=\"ltr\">today</code>.</li>\n<li><strong>distance</strong>: رقم أكبر من 0 و200 على الأكثر؛ والفاصلة العشرية مثل <code dir=\"ltr\">8,4</code> مقبولة.</li>\n<li><strong>duration</strong>: اختياري؛ وإن مُلئ فدقائق كاملة من 1 إلى 1440.</li>\n<li><strong>notes</strong>: اختياري؛ و500 حرف على الأكثر.</li>\n</ul>\n","hints":["ابنِ <code dir=\"ltr\">errors</code> حقلًا حقلًا بعبارات <code dir=\"ltr\">if</code>؛ وكل قاعدة تفشل تضبط <code dir=\"ltr\">errors.field = '…'</code>.","نصوص التاريخ بصيغة ISO تُقارَن بشكل صحيح كنصوص، فـ <code dir=\"ltr\">draft.date &gt; today</code> تعني «في المستقبل». ولرفض <code dir=\"ltr\">2026-02-30</code>، ابنِ <code dir=\"ltr\">new Date(Date.UTC(y, m - 1, d))</code> وتحقّق أن السنة والشهر واليوم تعود دون تغيير.","للمسافة، وحّد الصيغة أولًا (<code dir=\"ltr\">trim</code>، والفاصلة إلى نقطة)، ثم اختبرها مقابل <code dir=\"ltr\">/^\\d+(\\.\\d+)?$/</code> قبل التحقّق من النطاق."],"explanation":"<p>إبقاء التحقّق في دالة نقية تحوّل المسوّدة إلى كائن أخطاء يعني أن النموذج لا يحتاج إلا إلى <code dir=\"ltr\">Object.keys(errors).length === 0</code> ليقرّر هل يحفظ، وأن كل حقل يستطيع البحث عن رسالته الخاصة. وتمرير <code dir=\"ltr\">today</code> بدلًا من استدعاء <code dir=\"ltr\">new Date()</code> داخل الدالة يجعلها حتمية النتائج، فلا تنكسر الاختبارات عند منتصف الليل. وفحص الذهاب والإياب عبر <code dir=\"ltr\">Date.UTC</code> طريقة مختصرة لرفض التواريخ المستحيلة، التي كان <code dir=\"ltr\">Date</code> في JavaScript سيدحرجها بصمت إلى شهر مارس.</p>\n","checkLabels":{"valid-draft":"المسوّدة الكاملة المعقولة ليس فيها أخطاء","name":"تُرفض الأسماء الفارغة والطويلة جدًا","date":"تُرفض التواريخ سيئة الصيغة والمستحيلة والمستقبلية","distance":"تُرفض المسافات المفقودة وغير الرقمية والصفرية","duration":"المدّة المملوءة يجب أن تكون دقائق كاملة من 1 إلى 1440","several":"يُبلَّغ عن عدّة حقول غير صالحة دفعة واحدة"}},"words":636,"readMinutes":4}