سنضيف في هذا المقال اللمسات الأخيرة على وظائف تطبيق قائمة المهام Todo List الرئيسية -الذي بنيناه في المقالات السابقة- من خلال السماح بتعديل المهام الحالية، وترشيح Filtering قائمة المهام جميعها والمهام المكتملة وغير المكتملة فقط، كما سنتعرّف على التصيير الشرطي Conditional Rendering لواجهة المستخدِم UI.
- المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript ومعرفة استخدام سطر الأوامر أو الطرفية.
- الهدف: التعرف على التصيير الشرطي في React وتطبيق ترشيح القائمة وتعديل واجهة مستخدِم في تطبيقنا.
تعديل اسم المهمة
لا توجد لدينا واجهة مستخدِم لتعديل اسم المهمة حتى الآن، ولكن يمكننا على الأقل تنفيذ الدالة editTask()
في الملف App.js حاليًا، والتي تشبه الدالة deleteTask()
لأنها ستأخذ معرِّفًا id
للعثور على الكائن الهدف، وستأخذ الخاصية newName
التي تحتوي على الاسم الذي سنعدّل اسم المهمة إليه، في حين أننا سنستخدِم التابع Array.prototype.map()
بدلًا من التابع Array.prototype.filter()
لأننا نريد إعادة مصفوفة جديدة مع بعض التعديلات بدلًا من حذف شيء منها، لذا أضف الدالة editTask()
ضمن المكوِّن App
مع الدوال الأخرى كما يلي:
function editTask(id, newName) { const editedTaskList = tasks.map(task => { // إذا كانت هذه المهمة لها معرّف المهمة المعدّلة نفسه if (id === task.id) { // return {...task, name: newName} } return task; }); setTasks(editedTaskList); }
مرِّر editTask
إلى مكونات <Todo />
بوصفها خاصيةً Prop بالطريقة نفسها التي مررنا بها الخاصية deleteTask
كما يلي:
const taskList = tasks.map(task => ( <Todo id={task.id} name={task.name} completed={task.completed} key={task.id} toggleTaskCompleted={toggleTaskCompleted} deleteTask={deleteTask} editTask={editTask} /> ));
افتح الآن الملف Todo.js، إذ سنُجري إعادة بناء.
واجهة مستخدم التعديل
يجب توفير واجهة مستخدِم للمستخدِمين للسماح لهم بتعديل مهمة، لذا استورد أولًا الخطّاف useState
إلى المكوِّن Todo
كما فعلنا سابقًا مع المكوِّن App
عن طريق تعديل تعليمة الاستيراد الأولى إلى ما يلي:
import React, { useState } from "react";
سنضبط الآن الحالة isEditing
التي يجب أن تكون قيمتها الافتراضية false
، لذا أضف السطر التالي في الجزء العلوي من تعريف المكون Todo(props) { … }
:
const [isEditing, setEditing] = useState(false);
كما يجب إعادة التفكير في المكوِّن <Todo />
من الآن فصاعدًا، إذ نريد منه عرض أحد القالبَين التاليين بدلًا من القالب الوحيد المستخدَم حتى الآن:
- قالب العرض View عند عرض المهام فقط، وهو ما استخدمناه حتى الآن.
- قالب التعديل Editing عند تعديل المهام، وسننشئه بعد قليل.
انسخ الشيفرة التالية في الدالة Todo()
بعد الخطّاف useState()
وقبل التعليمة return
:
const editingTemplate = ( <form className="stack-small"> <div className="form-group"> <label className="todo-label" htmlFor={props.id}> New name for {props.name} </label> <input id={props.id} className="todo-text" type="text" /> </div> <div className="btn-group"> <button type="button" className="btn todo-cancel"> Cancel <span className="visually-hidden">renaming {props.name}</span> </button> <button type="submit" className="btn btn__primary todo-edit"> Save <span className="visually-hidden">new name for {props.name}</span> </button> </div> </form> ); const viewTemplate = ( <div className="stack-small"> <div className="c-cb"> <input id={props.id} type="checkbox" defaultChecked={props.completed} onChange={() => props.toggleTaskCompleted(props.id)} /> <label className="todo-label" htmlFor={props.id}> {props.name} </label> </div> <div className="btn-group"> <button type="button" className="btn"> Edit <span className="visually-hidden">{props.name}</span> </button> <button type="button" className="btn btn__danger" onClick={() => props.deleteTask(props.id)} > Delete <span className="visually-hidden">{props.name}</span> </button> </div> </div> );
أصبح لدينا الآن بنيتا قوالب مختلفتين -"Edit" و"View"- معرّفتان ضمن ثابتين منفصلين، وهذا يعني أنّ التعليمة return
الخاصة بالمكوِّن <Todo />
مكررة، فهي تحتوي على تعريف قالب العرض View أيضًا، ويمكن تنظيف هذا التكرار باستخدام التصيير الشرطي Conditional Rendering لتحديد القالب الذي يعيده المكوِّن، وبالتالي يُصيَّر في واجهة المستخدِم.
التصيير الشرطي
يمكننا في صيغة JSX استخدام شرط لتغيير ما يصيّره المتصفح، إذ يُكتَب الشرط في صيغة JSX باستخدام معامِل ثلاثي Ternary Operator، فالشرط في حالة المكوِّن <Todo />
هو "هل تُعدَّل هذه المهمة؟"، لذا عدِّل التعليمة return
ضمن الدالة Todo()
، بحيث تصبح كما يلي:
return <li className="todo">{isEditing ? editingTemplate : viewTemplate}</li>;
يجب أن يصيّر متصفحك جميع مهامك كما كانت سابقًا، ويمكن مشاهدة قالب التعديل من خلال تغيير القيمة الافتراضية للحالة isEditing
من false
إلى true
في شيفرتك حاليًا، إذ سنجعل زر التعديل Edit يبدِّل هذه القيمة لاحقًا.
التبديل بين قوالب
سنجعل الآن ميزة التعديل تفاعليةً، إذ يجب أولًا استدعاء الدالة setEditing()
مع القيمة true
عندما يضغط المستخدِم على زر التعديل Edit في قالب العرض viewTemplate
لنتمكّن من التبديل بين القوالب، لذا عدِّل زر التعديل Edit في قالب العرض viewTemplate
كما يلي:
<button type="button" className="btn" onClick={() => setEditing(true)}> Edit <span className="visually-hidden">{props.name}</span> </button>
سنضيف الآن السمة onClick
نفسها إلى زر الإلغاء Cancel في قالب التعديل editingTemplate
، ولكن سنضبط الحالة isEditing
هذه المرة على القيمة false
لتعيدنا إلى قالب العرض، لذا عدِّل زر الإلغاء Cancel في قالب التعديل editingTemplate
كما يلي:
<button type="button" className="btn todo-cancel" onClick={() => setEditing(false)} > Cancel <span className="visually-hidden">renaming {props.name}</span> </button>
يجب أن تكون قادرًا الآن على الضغط على زرَي التعديل Edit والإلغاء Cancel في عناصر المهام للتبديل بين القالبين.
الخطوة التالية هي جعل وظيفة التعديل تعمل فعليًا.
التعديل من واجهة المستخدم
ما سنعمل عليه تاليًا مماثل لما عملنا عليه مع المكون Form
(انظر المقال السابق تقسيم تطبيق React إلى مكونات).
إذا كتب المستخدِم شيئًا في حقل الإدخال الجديد، فيجب تتبّع النص الذي يدخله، ويجب استخدام خاصية رد النداء Callback Prop بمجرد إرسال النموذج لتحديث الحالة باسم المهمة الجديد، إذ سنبدأ بإنشاء خطّاف Hook جديد لتخزين وضبط الاسم الجديد، لذا ضع ما يلي أسفل الخطّاف الموجود مسبقًا في الملف Todo.js:
const [newName, setNewName] = useState('');
أنشئ بعد ذلك الدالة handleChange()
التي تضبط الاسم الجديد، لذا ضع ما يلي بعد الخطافات وقبل القوالب:
function handleChange(e) { setNewName(e.target.value); }
سنعدِّل الآن الحقل <input />
الخاص بقالب التعديل editingTemplate
بضبط السمة value
على newName
وربط الدالة handleChange()
مع الحدث onChange
كما يلي:
<input id={props.id} className="todo-text" type="text" value={newName} onChange={handleChange} />
يجب أخيرًا إنشاء دالة تعالِج الحدث onSubmit
الخاصة بنموذج التعديل، لذا أضف ما يلي مباشرةً بعد الدالة السابقة التي أضفتها:
function handleSubmit(e) { e.preventDefault(); props.editTask(props.id, newName); setNewName(""); setEditing(false); }
تذكّر أنّ خاصية رد النداء editTask()
تحتاج إلى معرِّف المهمة التي نعدّلها بالإضافة إلى اسمها الجديد.
اربط الدالة handleSubmit()
مع حدث إرسال submit
النموذج عبر إضافة معالج الحدث onSubmit
التالي إلى عنصر النموذج <form>
الخاص بقالب التعديل editingTemplate
:
<form className="stack-small" onSubmit={handleSubmit}>
يجب الآن أن تكون قادرًا على تعديل مهمة في متصفحك.
العودة إلى أزرار الترشيح
اكتملت الآن ميزاتنا الرئيسية، وبالتالي يمكننا التفكير في أزرار الترشيح التي تكرّر العنوان "All" حاليًا بدون وظائف تطبّقها، إذ سنعيد تطبيق بعض المهارات التي استخدمناها في المكوِّن <Todo />
من أجل ما يلي:
- إنشاء خطّاف لتخزين المرشِّح Filter النشط.
-
تصيير مصفوفة من عناصر
<FilterButton />
التي تسمح للمستخدِمِين بتغيير المرشِّح النشط بين جميع المهام والمهام المكتملة والمهام غير المكتملة.
إضافة خطاف ترشيح
أضف خطّافًا جديدًا إلى الدالة App()
التي تقرأ وتضبط المرشِّح، إذ نريد أن يكون المرشّح الافتراضي هو All
لأنه يجب عرض جميع المهام في البداية.
const [filter, setFilter] = useState('All');
تعريف المرشحات
هدفنا الآن هو:
- يجب أن يكون لكل مرشِّح اسم فريد.
- يجب أن يكون لكل مرشِّح سلوك فريد.
يُعَدّ كائن JavaScript طريقةً رائعةً لربط الأسماء بالسلوكيات، فالمفتاح هو اسم المرشِّح، والخاصية هي السلوك المرتبط بهذا الاسم.
أضِف كائنًا بالاسم FILTER_MAP
في الجزء العلوي من الملف App.js بعد تعليمات الاستيراد وقبل الدالة App()
كما يلي:
const FILTER_MAP = { All: () => true, Active: task => !task.completed, Completed: task => task.completed };
تُعَدّ قيم الكائن FILTER_MAP
دوالًا سنستخدِمها لترشيح مصفوفة بيانات المهام tasks
كما يلي:
-
يعرِض المرشِّح
All
جميع المهام، لذلك يعيد القيمةtrue
لجميع المهام. -
يعرِض المرشِّح
Active
المهام التي يكون فيها للخاصيةcompleted
القيمةfalse
. -
يعرِض المرشِّح
Completed
المهام التي يكون فيها للخاصيةcompleted
القيمةtrue
.
أضف ما يلي بعد الذي أضفناه منذ قليل، إذ سنستخدِم التابع Object.keys()
لتجميع مصفوفة FILTER_NAMES
:
const FILTER_NAMES = Object.keys(FILTER_MAP);
ملاحظة: نعرِّف هذه الثوابت خارج الدالة App()
لأنها إذا عُرِّفت ضمنها، فسيُعاد حسابها في كل مرة يعاد فيها تصيير المكوِّن <App />
، ولا نريد حصول ذلك، فلن تتغير هذه المعلومات أبدًا بغض النظر عمّا يفعله تطبيقنا.
تصيير المرشحات
أصبح لدينا الآن مصفوفة FILTER_NAMES
، وبالتالي يمكننا استخدامها لتصيير المرشِّحات الثلاثة، إذ يمكننا إنشاء ثابت يسمى filterList
ضمن الدالة App()
، إذ سنستخدِم هذا الثابت لربط مصفوفة الأسماء وإعادة المكوِّن <FilterButton />
، وتذكّر أننا هنا بحاجة إلى مفاتيح أيضًا، لذا أضف ما يلي بعد التصريح عن الثابت taskList
:
const filterList = FILTER_NAMES.map(name => ( <FilterButton key={name} name={name}/> ));
سنستبدل الآن الثابت filterList
بالمكونات <FilterButton />
الثلاثة المكرَّرة في الملف App.js، أي بدلًا مما يلي:
<FilterButton /> <FilterButton /> <FilterButton />
ضع التالي:
{filterList}
لن يعمل ذلك الآن، إذ لدينا المزيد لعمله أولًا.
المرشحات التفاعلية
يمكنك جعل أزرار المرشِّحات تفاعليةً من خلال تحديد الخاصيات التي تحتاج إلى استخدامها.
-
نعلم أن المكوِّن
<FilterButton />
يجب أن يبلّغ عمّا إذا كان مضغوطًا حاليًا، ويجب الضغط عليه إذا تطابق اسمه مع القيمة الحالية لحالة المرشِّح. -
نعلم أن المكوِّن
<FilterButton />
يحتاج إلى دالة رد نداء لضبط المرشِّح النشط، كما يمكننا الاستفادة مباشرةً من الخطّافsetFilter
.
عدِّل الثابت filterList
كما يلي:
const filterList = FILTER_NAMES.map(name => ( <FilterButton key={name} name={name} isPressed={name === filter} setFilter={setFilter} /> ));
كما يجب الآن تعديل الملف FilterButton.js لاستخدام الخاصيات التي قدمناها له بالطريقة نفسها التي طبّقناها سابقًا مع المكوِّن <Todo />
، لذا طبّق ما يلي وتذكَّر استخدام الأقواس المعقوصة لقراءة هذه المتغيرات:
-
ضع
all
مكان الخاصية{props.name}
. -
اضبط قيمة السمة
aria-pressed
على الخاصية{props.isPressed}
. -
أضف السمة
onClick
التي تستدعي الخطّافprops.setFilter()
مع اسم المرشِّح.
يجب أن تكون الآن الدالة FilterButton()
كما يلي:
function FilterButton(props) { return ( <button type="button" className="btn toggle-btn" aria-pressed={props.isPressed} onClick={() => props.setFilter(props.name)} > <span className="visually-hidden">Show </span> <span>{props.name}</span> <span className="visually-hidden"> tasks</span> </button> ); }
انتقل إلى متصفحك مرةً أخرى، إذ يجب أن ترى تسمية الأزرار المختلفة بأسمائها الخاصة، فإذا ضغطتَ على زر الترشيح Filter، فيجب أن ترى نَص الزر يأخذ تخطيطًا جديدًا، إذ يخبرك هذا بأنه مُحدَّد، فإذا ألقيت نظرةً على فاحص صفحة أدوات التطوير DevTool’s Page Inspector أثناء النقر على الأزرار، فسترى أنّ قيم السمة aria-pressed
تتغير وفقًا لذلك.
لكن لا تزال الأزرار لا ترشِّح المهام في واجهة المستخدِم.
ترشيح المهام في واجهة المستخدم
يربط الثابت taskList
في الدالة App()
حاليًا حالة المهام ويعيد مكوِّن <Todo />
جديدًا لكل منها، ولكننا لا نريد ذلك، إذ يجب تصيير المهمة فقط إذا كانت مُضمَّنةً في نتائج تطبيق المرشِّح المحدَّد، وبالتالي يجب ترشيح حالة المهام باستخدام التابع Array.prototype.filter()
قبل ربطها لإزالة الكائنات التي لا نريد تصييرها، لذا عدِّل الثابت taskList
كما يلي:
const taskList = tasks .filter(FILTER_MAP[filter]) .map(task => ( <Todo id={task.id} name={task.name} completed={task.completed} key={task.id} toggleTaskCompleted={toggleTaskCompleted} deleteTask={deleteTask} editTask={editTask} /> ));
يمكن الوصول إلى قيمة في المصفوفة FILTER_MAP
التي تتوافق مع مفتاح حالة المرشِّح لتحديد دالة رد النداء التي يجب استخدامها في التابع Array.prototype.filter()
، فإذا كان المرشِّح هو All
مثلًا، فسيُقيَّم العنصر FILTER_MAP[filter]
على () => true
، كما يؤدي اختيار المرشِّح في متصفحك الآن إلى إزالة المهام التي لا تفي بمعاييره، في حين سيتغير العدد الموجود في العنوان أعلى القائمة ليمثِّل القائمة.
الخلاصة
اكتمل تطبيقنا الآن، ولكن يمكننا إجراء بعض التحسينات لضمان إمكانية استخدام مجموعة أكبر من المستخدِمين له، كما يتناول المقال التالي تضمين إدارة التركيز Focus Management في React التي يمكنها تحسين قابلية الاستخدام وتقليل الارتباك لكل من مستخدِمي لوحة المفاتيح فقط وقارئات الشاشة.
ترجمة -وبتصرُّف- للمقال React interactivity: Editing, filtering, conditional rendering.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.