full_stack_101 إنشاء النماذج (forms) في React


ابراهيم الخضور

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

import React, { useState } from 'react'import Note from './components/Note'

const App = (props) => {  const [notes, setNotes] = useState(props.notes)
  return (
    <div>
      <h1>Notes</h1>
      <ul>
        {notes.map(note => 
          <Note key={note.id} note={note} />
        )}
      </ul>
    </div>
  )
}

export default App 

يستخدم المكوِّن الدالة useState لتهيئة قطع الحالة المخزنة في notes بمصفوفة الملاحظات التي تمرر كخصائص:

const App = (props) => { 
  const [notes, setNotes] = useState(props.notes) 

  // ...
}

علينا استخدام مصفوفة فارغة لتهيئة الملاحظات إن أردنا أن نبدأ التطبيق بقائمة فارغة من الملاحظات. سنحذف أيضًا الخصائص props من تعريف الدالة طالما أنها لن تُستخدم:

const App = () => { 
  const [notes, setNotes] = useState([]) 

  // ...
}  

لنركز الآن على القيم الأولية التي تمرر كخصائص للدالة، حيث سنضيف نموذج HTML إلى المكوِّن الذي سيضيف الملاحظات الجديدة:

const App = (props) => {
  const [notes, setNotes] = useState(props.notes)

  const addNote = (event) => {
    event.preventDefault()
    console.log('button clicked', event.target)
  }
  return (
    <div>
      <h1>Notes</h1>
      <ul>
        {notes.map(note => 
          <Note key={note.id} note={note} />
        )}
      </ul>
      <form onSubmit={addNote}>
        <input />
        <button type="submit">save</button>
      </form>
       </div>
  )
}

لقد أضفنا أيضًا الدالة addNote كمعالج حدث يُستدعى عندما يُسلّم (submit) النموذج عند النقر على زر التسليم. سنستخدم الطريقة التي ناقشناها في [القسم 1]() لتعريف معالج الحدث:

const addNote = (event) => {
  event.preventDefault()
  console.log('button clicked', event.target)
}

يؤدي المعامل event دور الحدث الذي يستدعي دالة المعالج. يستدعي معالج الأحداث التابع ()event.preventDefaultr الذي يمنع حدوث التسليم الافتراضي للنموذج، والذي يسبب إعادة تحميل الصفحة إضافة إلى عدة أشياء أخرى. لنطبع الآن وجهة الحدث event.target على الطرفية:

eveny_target_001.png

إنّ النموذج الذي عرّفناه في المكوًّن هو وجهة الحدث كما هو واضح، لكن كيف سنصل إلى البيانات الموجودة ضمن عنصر الإدخال input (وهو مربع النص في حالتنا) في النموذج؟ طرق كثيرة يمكن أن تفي بالغرض. سنلقي نظرة على أولها وهي استخدام المكوِّنات المقادة (controlled components). لنضف الآن قطعة حالة جديدة تدعى newNote لتخزّن ما يدخله المستخدم في العنصر input ضمن الصفة value:

const App = (props) => {
  const [notes, setNotes] = useState(props.notes)
  const [newNote, setNewNote] = useState(    'a new note...'  ) 
  const addNote = (event) => {
    event.preventDefault()
    console.log('button clicked', event.target)
  }

  return (
    <div>
      <h1>Notes</h1>
      <ul>
        {notes.map(note => 
          <Note key={note.id} note={note} />
        )}
      </ul>
      <form onSubmit={addNote}>
        <input value={newNote} />
        <button type="submit">save</button>
      </form>   
    </div>
  )
}

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

onchange_missing_warning_002.png

طالما أننا أسندنا قطعة من حالة المكوِّن App إلى الصفة value لعنصر الإدخال، فسيتحكم المكوِّن الآن بسلوك هذا العنصر. لذا لابد من تعريف معالج حدث جديد يزامن التغييرات التي تحدث في عنصر الإدخال مع حالة المكوِّن إن أردنا الكتابة ضمنه:

const App = (props) => {
  const [notes, setNotes] = useState(props.notes)
  const [newNote, setNewNote] = useState(
    'a new note...'
  ) 

  // ...

  const handleNoteChange = (event) => {    console.log(event.target.value)    setNewNote(event.target.value)  }
  return (
    <div>
      <h1>Notes</h1>
      <ul>
        {notes.map(note => 
          <Note key={note.id} note={note} />
        )}
      </ul>
      <form onSubmit={addNote}>
        <input
          value={newNote}
          onChange={handleNoteChange}        />
        <button type="submit">save</button>
      </form>   
    </div>
  )
}

سنعرف الآن معالج حدث للخاصية onChange لعنصر إدخال النموذج:

<input
  value={newNote}
  onChange={handleNoteChange}
/>

سيُستدعى الآن معالج الحدث عند حدوث أية تغييرات في عنصر الإدخال، وستتلقى دالة المعالج كائن الحدث كمعامل:

const handleNoteChange = (event) => {
  console.log(event.target.value)
  setNewNote(event.target.value)
}

ترتبط الآن الخاصية target لكائن الحدث بعنصر الإدخال المُقاد (من قِبل المكوِّن)، وستشير القيمة event.target.value إلى محتوى عنصر الإدخال.

ملاحظة: لا داعي لاستدعاء التابع ()event.preventDefault كما فعلنا سابقًا، لعدم وجود عمليات افتراضية عند حدوث تغيرات على عنصر الإدخال.

يمكنك تتبع ما يجري عند استدعاء المعالج ضمن النافذة console في الطرفية:

console_track_changes_003.png

سترى كيف تتغير الحالة مباشرة داخل نافذة React Devtools، طبعًا إن كنت قد ثبتها سابقًا.

react_dev_tools_004.png

ستعكس قطعة الحالة newNote في المكوِّن App التغيرات في قيمة عنصر الإدخال، إذًا يمكننا إكمال الدالة addNote التي تضيف ملاحظة جديدة:

const addNote = (event) => {
  event.preventDefault()
  const noteObject = {
    content: newNote,
    date: new Date().toISOString(),
    important: Math.random() < 0.5,
    id: notes.length + 1,
  }

  setNotes(notes.concat(noteObject))
  setNewNote('')
}

سننشئ أولًا كائنًا جديدًا يدعى noteObject يتلقى محتواه من قيمة الحالة newState. بينما يتولّد المعرِّف الفريد id وفقًا لعدد الملاحظات. سينجح هذا الأسلوب في تطبيقنا طالما أنه لن يستخدم لحذف الملاحظات. سنعطي الملاحظة احتمالًا بنسبة 50% لأن تعتبر مهمة بالاستفادة من الدالة ()Math.random وهي دالة تستخدم لتوليد أرقام عشوائية.

نستخدم التابع concat الذي تعرفنا عليه سابقًا في إضافة الملاحظة الجديدة إلى قائمة الملاحظات:

setNotes(notes.concat(noteObject))

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

setNewNote('')

ستجد شيفرة التطبيق الذي نعمل عليه في المسار part2-2 ضمن المخزن المخصص على github.

انتقاء العناصر التي ستعرض

سنضيف مهمة جديدة للتطبيق تقتضي إظهار الملاحظات الهامة فقط. ولنبدأ بإضافة قطعة حالة جديدة للمكوِّن App مهمتها تحديد الملاحظات التي يجب أن تعرض:

const App = (props) => {
  const [notes, setNotes] = useState(props.notes) 
  const [newNote, setNewNote] = useState('')
  const [showAll, setShowAll] = useState(true)  
  // ...
}

سنعدل المكوًن أيضًا لكي يخزن قائمة بكل الملاحظات التي ستعرض ضمن المتغيّر notesDirectShow. يعتمد اختيار عناصر القائمة على حالة المكوِّن:

import React, { useState } from 'react'
import Note from './components/Note'

const App = (props) => {
  const [notes, setNotes] = useState(props.notes)
  const [newNote, setNewNote] = useState('') 
  const [showAll, setShowAll] = useState(true)

  // ...

  const notesToShow = showAll    
    ? notes    
    : notes.filter(note => note.important === true)
  return (
    <div>
      <h1>Notes</h1>
      <ul>
        {notesToShow.map(note =>
          <Note key={note.id} note={note} />
        )}
      </ul>
      // ...
    </div>
  )
}

يظهر تعريف المتغيّر notesDirectShow مختصرًا بالشكل التالي:

const notesToShow = showAll
  ? notes
  : notes.filter(note => note.important === true)

سترى في التعريف العامل الشرطي الذي ستراه أيضًا في عدة لغات برمجة أخرى. وسنشرح منطق هذا العامل من خلال المثال التالي:

const result = condition ? val1 : val2

تعمل هذه الصيغة كالتالي: سيأخذ المتغير result القيمة val1 إذا تحقق الشرط condition وإلا سيأخذ القيمة val2.

وبالعودة إلى الصيغة الموجودة في شيفرة التطبيق، سيأخذ المتغيّر notesToShow القيمة notes، وستظهر كل الملاحظات، إذا كان الشرط showAll محققًا. وإلا سيأخذ القيمة الأخرى، وستظهر الملاحظات الهامة فقط.

لاحظ أن عملية الانتقاء قد تمت بمساعدة تابع الانتقاء في المصفوفات filter.

notes.filter(note => note.important === true)

ليس لعامل الموازنة (===) أهمية طالما أن القيمة note.important من النمط المنطقي. إذًا يمكننا ببساطة كتابة السطر البرمجي السابق بالشكل:

notes.filter(note => note.important)

إن المغزى من كتابة عامل الموازنة بالشكل(===) هو توضيح ناحية هامة بأن العامل (==) لا يؤدي دوره بالشكل المطلوب في كل الحالات ويجب عدم استخدامه في المقارنة حصرًا. اقرأ المزيد حول الموضوع لفهمٍ أعمق. يمكنك اختبار انتقاء الملاحظات بجعل القيمة الأولية false لقطعة الحالة showAll.

سنقوم تاليًا بمنح المستخدم إمكانية تغيير قيمة الحالة showAll من واجهة المستخدم. وستظهر الشيفرة بعد التعديل كما يلي:

import React, { useState } from 'react' 
import Note from './components/Note'

const App = (props) => {
  const [notes, setNotes] = useState(props.notes) 
  const [newNote, setNewNote] = useState('')
  const [showAll, setShowAll] = useState(true)

  // ...

  return (
    <div>
      <h1>Notes</h1>
      <div>
        <button onClick={() => setShowAll(!showAll)}>
          show {showAll ? 'important' : 'all' }
        </button>
      </div>
      <ul>
        {notesToShow.map(note =>
          <Note key={note.id} note={note} />
        )}
      </ul>
      // ...    
    </div>
  )
}

سنتحكم بإخفاء أو إظهار الملاحظات باستخدام زر، وسيكون معالج الحدث الخاص به بسيطًا بحيث يمكن تعريفه مباشرة داخل الصفة onClick. يبدّل معالج الحدث قيمة showAll عند كل نقرة زر.

() => setShowAll(!showAll)

يتغير النص المكتوب على الزر حسب الحالة فمرة سيكون "important" ومرة "all":

show {showAll ? 'important' : 'all'}

ستجد شيفرة التطبيق الذي نعمل عليه في المسار part2-3 ضمن المخزن المخصص على github.

التمارين 2.6-2.10

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

تحذير: تنشئ الأداة create-react-app مستودع git محلي يحتوي المشروع، إلا إن كان في المجلد مستودع محلي سابق. من المرجح أنك لا تريد أن يغدو المشروع مستودعًا، لهذا نفذ الأمر التاليrm -rf .git في مسار المشروع.

2.6 دليل الهاتف: الخطوة 1

صمم دليل هاتف بسيط. سنعمل في هذا القسم على إضافة الأسماء فقط. اكتب الشيفرة المناسبة لإضافة شخص إلى دليلك. يمكنك استخدام الشيفرة التالية كنقطة انطلاق للمكوِّن App الخاص بتطبيقك:

import React, { useState } from 'react'

const App = () => {
  const [ persons, setPersons ] = useState([
    { name: 'Arto Hellas' }
  ]) 
  const [ newName, setNewName ] = useState('')

  return (
    <div>
      <h2>Phonebook</h2>
      <form>
        <div>
          name: <input />
        </div>
        <div>
          <button type="submit">add</button>
        </div>
      </form>
      <h2>Numbers</h2>
      ...
    </div>
  )
}

export default App

تستخدم قطعة الحالة newName في قيادة عنصر الإدخال في النموذج. من المفيد أحيانًا تصيير قطع الحالة أو المتغيرات لتظهر قيمها بشكل نصي لأغراض التنقيح. إذ يمكنك إضافة الشيفرة التالية مؤقتًا إلى العنصر الذي تصّيره:

<div>debug: {newName}</div>

لا تنس الاستفادة مما تعلمته عن تنقيح تطبيقات React في القسم 1، كما سيمنحك الموسِّع React developer tools قدرة كبيرة على تتبع التغيرات التي تحدث في حالة التطبيق. بعد إتمامك العمل ستبدو واجهة التطبيق مشابهة للواجهة التالية:

phonebook_step1_005.png

لاحظ في الشكل السابق كيف يُستخدم الموسِّع الذي ذكرناه.

تذكر:

  • يمكنك أن تضع اسم الشخص كقيمة للخاصية key.
  • تذكر أن تمنع التسليم الافتراضي للنموذج.

2.7 دليل الهاتف: الخطوة 2

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

phonebook_step2_006.png

تلميح: يفضل استخدام القالب النصي إذا أردت أن تضع قيمة متغير ما ضمن سلسلة نصية.

`${newName} is already added to phonebook`

فإن كانت قيمة المتغير newName هي Arto Hellas سيعيد القالب النصي ما يلي:

`Arto Hellas is already added to phonebook`

كما يمكن استعمال إشارة (+) بكل بساطة:

newName + ' is already added to phonebook'

طبعًا ستبدو شيفرتك إحترافية أكثر عندما تستخدم القالب النصي.

2.8 دليل الهاتف: الخطوة 3

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

<form>
  <div>name: <input /></div>
  <div>number: <input /></div>
  <div><button type="submit">add</button></div>
</form>

حتى هذه النقطة ستبدو واجهة التطبيق مشابه للصورة التالية:

phonebook_step3_007.png

لاحظ كيف يقوم الموسٍّع بعمله!

2.9 دليل الهاتف: الخطوة 4 *

أضف للتطبيق إمكانية البحث عن الأشخاص بالاسم:

phonebook_step4_008.png

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

تذكر: أضف دائمًا بعض البيانات الجاهزة في تطبيقك كما فعلنا في الشيفرة التالية:

const App = () => {
  const [persons, setPersons] = useState([
    { name: 'Arto Hellas', number: '040-123456' },
    { name: 'Ada Lovelace', number: '39-44-5323523' },
    { name: 'Dan Abramov', number: '12-43-234345' },
    { name: 'Mary Poppendieck', number: '39-23-6423122' }
  ])

  // ...
}

سيوفر عليك ذلك عناء إدخال البيانات يدويًا كل مرة عند اختبار وظيفة جديدة.

2.10 دليل الهاتف: الخطوة 5

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

يكفي أن تشكل ثلاثة مكوِّنات جديدة. يمكنك على سبيل المثال عزل عملية البحث في مكوِّن وكذلك النموذج الذي يُستخدم لإضافة الأشخاص وأيضًا عملية تصيير كل الأشخاص في الدليل أو عملية تصيير تفاصيل شخص واحد.

يمكن أن يشبه المكوِّن الجذري بعد إعادة البناء الشكل التالي:

const App = () => {
  // ...

  return (
    <div>
      <h2>Phonebook</h2>

      <Filter ... />

      <h3>Add a new</h3>

      <PersonForm 
        ...
      />

      <h3>Numbers</h3>

      <Persons ... />
    </div>
  )
}

سيصيّر المكوِّن الجذري عنوان التطبيق فقط، وتتولى المكوِّنات الجديدة بقية المهام.

تذكر: قد تواجهك المشاكل في هذا التمرين إن لم تعرف المكوِّنات في المكان الصحيح، ولعلك ستستفيد الآن من مراجعة الفقرة لا تعرف مكوِّنًا داخل مكوِّن آخر من القسم السابق.

ترجمة -وبتصرف- للفصل Forms من سلسلة Deep Dive Into Modern Web Development





تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن