اذهب إلى المحتوى

سنضيف في هذا المقال اللمسات الأخيرة على وظائف تطبيق قائمة المهام Todo List الرئيسية -الذي بنيناه في المقالات السابقة- من خلال السماح بتعديل المهام الحالية، وترشيح Filtering قائمة المهام جميعها والمهام المكتملة وغير المكتملة فقط، كما سنتعرّف على التصيير الشرطي Conditional Rendering لواجهة المستخدِم UI.

تعديل اسم المهمة

لا توجد لدينا واجهة مستخدِم لتعديل اسم المهمة حتى الآن، ولكن يمكننا على الأقل تنفيذ الدالة 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 في عناصر المهام للتبديل بين القالبين.

01_edit.png

الخطوة التالية هي جعل وظيفة التعديل تعمل فعليًا.

التعديل من واجهة المستخدم

ما سنعمل عليه تاليًا مماثل لما عملنا عليه مع المكون 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 تتغير وفقًا لذلك.

02_filter-buttons.png

لكن لا تزال الأزرار لا ترشِّح المهام في واجهة المستخدِم.

ترشيح المهام في واجهة المستخدم

يربط الثابت 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، كما يؤدي اختيار المرشِّح في متصفحك الآن إلى إزالة المهام التي لا تفي بمعاييره، في حين سيتغير العدد الموجود في العنوان أعلى القائمة ليمثِّل القائمة.

03_filtered-todo-list.png

الخلاصة

اكتمل تطبيقنا الآن، ولكن يمكننا إجراء بعض التحسينات لضمان إمكانية استخدام مجموعة أكبر من المستخدِمين له، كما يتناول المقال التالي تضمين إدارة التركيز Focus Management في React التي يمكنها تحسين قابلية الاستخدام وتقليل الارتباك لكل من مستخدِمي لوحة المفاتيح فقط وقارئات الشاشة.

ترجمة -وبتصرُّف- للمقال React interactivity: Editing, filtering, conditional rendering.

اقرأ أيضًا


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

أفضل التعليقات

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



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...