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

إنشاء تطبيق قائمة مهام باستخدام React


Ola Abbas

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

اقتباس

ملاحظة: إذا أردت التحقق من شيفرتك، فيمكنك العثور على إصدار نهائي من نموذج شيفرتنا لتطبيق React في المستودع todo-react، وراجع هذا الرابط للحصول على إصدار حي ومباشر.

قصص مستخدم التطبيق

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

  • قراءة قائمة المهام.
  • إضافة مهمة باستخدام الفأرة أو لوحة المفاتيح.
  • وضْع علامة على المهام المكتملة باستخدام الفأرة أو لوحة المفاتيح.
  • حذف أيّ مهمة باستخدام الفأرة أو لوحة المفاتيح.
  • تعديل أيّ مهمة باستخدام الفأرة أو لوحة المفاتيح.
  • عرض مجموعة فرعية محدَّدة من المهام: جميع المهام، أو المهمة النشطة فقط، أو المهام المكتملة فقط.

تجهيز المشروع الأولي

أنشأتْ الأداة create-react-app بعض الملفات التي لن نستخدِمها مطلقًا في مشروعنا، إذ لن نضيف ملف تنسيق سابق لعرض المكونات، لذا احذف أولًا استيراد App.css من أعلى الملف App.js، كما أننا لن نستخِدم الملف logo.svg، لذا أزِل استيراده أيضًا، ثم انسخ والصق بعد ذلك الأوامر التالية في طرفيتك لحذف بعض الملفات غير الضرورية، وتأكّد من أنك تبدأ من المجلد الجذر للتطبيق:

# ‫انتقل إلى المجلد src الخاص بمشروعك
cd src
# احذف بعض الملفات
rm -- App.test.js App.css logo.svg serviceWorker.js setupTests.js
# انتقل احتياطيًا إلى جذر المشروع
cd ..

ملاحظتان:

  • هناك ملفان من الملفات التي حذفناها مخصَّصان لاختبار التطبيق، وبالتالي لن نغطّي الاختبار في مثالنا.
  • إذا أوقفت خادمك لتنفيذ المهام السابقة في الطرفية، فيجب تشغيله مرةً أخرى باستخدام الأمر npm start.

شيفرة المشروع الأساسية

سنقدِّم فيما يلي شيفرة الدالة App()‎ وشيفرة CSS لتنسيق تطبيقك لتستخدمها بدلًا من الشيفرة التي لديك الآن.

صيغة JSX

انسخ مقتطف الشيفرة التالي إلى مفكرتك، ثم الصقه في الملف App.js بحيث يحل محل دالة App()‎ الحالية:

function App(props) {
  return (
    <div className="todoapp stack-large">
      <h1>TodoMatic</h1>
      <form>
        <h2 className="label-wrapper">
          <label htmlFor="new-todo-input" className="label__lg">
            What needs to be done?
          </label>
        </h2>
        <input
          type="text"
          id="new-todo-input"
          className="input input__lg"
          name="text"
          autoComplete="off"
        />
        <button type="submit" className="btn btn__primary btn__lg">
          Add
        </button>
      </form>
      <div className="filters btn-group stack-exception">
        <button type="button" className="btn toggle-btn" aria-pressed="true">
          <span className="visually-hidden">Show </span>
          <span>all</span>
          <span className="visually-hidden"> tasks</span>
        </button>
        <button type="button" className="btn toggle-btn" aria-pressed="false">
          <span className="visually-hidden">Show </span>
          <span>Active</span>
          <span className="visually-hidden"> tasks</span>
        </button>
        <button type="button" className="btn toggle-btn" aria-pressed="false">
          <span className="visually-hidden">Show </span>
          <span>Completed</span>
          <span className="visually-hidden"> tasks</span>
        </button>
      </div>
      <h2 id="list-heading">
        3 tasks remaining
      </h2>
      <ul
        role="list"
        className="todo-list stack-large stack-exception"
        aria-labelledby="list-heading"
      >
        <li className="todo stack-small">
          <div className="c-cb">
            <input id="todo-0" type="checkbox" defaultChecked={true} />
            <label className="todo-label" htmlFor="todo-0">
              Eat
            </label>
          </div>
          <div className="btn-group">
            <button type="button" className="btn">
              Edit <span className="visually-hidden">Eat</span>
            </button>
            <button type="button" className="btn btn__danger">
              Delete <span className="visually-hidden">Eat</span>
            </button>
          </div>
        </li>
        <li className="todo stack-small">
          <div className="c-cb">
            <input id="todo-1" type="checkbox" />
            <label className="todo-label" htmlFor="todo-1">
              Sleep
            </label>
          </div>
          <div className="btn-group">
            <button type="button" className="btn">
              Edit <span className="visually-hidden">Sleep</span>
            </button>
            <button type="button" className="btn btn__danger">
              Delete <span className="visually-hidden">Sleep</span>
            </button>
          </div>
        </li>
        <li className="todo stack-small">
          <div className="c-cb">
            <input id="todo-2" type="checkbox" />
            <label className="todo-label" htmlFor="todo-2">
              Repeat
            </label>
          </div>
          <div className="btn-group">
            <button type="button" className="btn">
              Edit <span className="visually-hidden">Repeat</span>
            </button>
            <button type="button" className="btn btn__danger">
              Delete <span className="visually-hidden">Repeat</span>
            </button>
          </div>
        </li>
      </ul>
    </div>
  );
}

افتح الآن الملف public/index.html وعدّل نص العنصر <title> ليصبح TodoMatic، بحيث يطابق العنصر <h1> الموجود أعلى تطبيقنا.

<title>TodoMatic</title>

يجب أن ترى ما يلي عند تحديث متصفحك:

01_unstyled-app.png

لا يبدو هذا التطبيق جميلًا ولا يعمل بعد، لكنه جيد حاليًا، وضَع في بالك شيفرة JSX، وكيفية توافقها مع قصص المستخدِمين:

  • لدينا عنصر <form> مع العنصر <input type="text"‎> لكتابة مهمة جديدة، وزر لإرسال النموذج.
  • لدينا مجموعة من الأزرار التي سنستخدِمها لمهامنا.
  • لدينا عنوان heading يخبرنا عن عدد المهام المتبقية.
  • لدينا ثلاث مهام مرتبة ضمن قائمة غير مرتبة، إذ تُعَدّ كل مهمة أنها عنصر قائمة <li>، كما تحتوي على أزرار لتعديلها وحذفها، بالإضافة إلى مربع اختيار لإيقاف تشغيلها.

سيسمح النموذج بإنشاء المهام، إذ تسمح الأزرار بترشيح المهام، ويُعَدّ العنوان والقائمة طريقةً لقراءتها، إذ ليست واجهة المستخدِم المُستخدَمة لتعديل مهمة موجودةً في الوقت الحالي، ولكن سنكتبها لاحقًا.

ميزات الشمولية

قد تلاحظ بعض السمات غير العادية هنا مثل:

<button type="button" className="btn toggle-btn" aria-pressed="true">
  <span className="visually-hidden">Show </span>
  <span>all</span>
  <span className="visually-hidden"> tasks</span>
</button>

تخبر السمة aria-pressed التقنيات المساعدة مثل قارئات الشاشة أنه قد يكون الزر في إحدى حالتين وهما مضغوط pressed أو غير مضغوط unpressed، إذ تمثِّلان حالة التشغيل on والإيقاف off، ويعني تعيين القيمة true أنّ الزر مضغوط افتراضيًا.

ليس للصنف visually-hidden أيَّ تأثير حتى الآن، لأننا لم نضمِّن شيفرة CSS، فإذا وضعنا التنسيقات في مكانها الصحيح، فسيُخفَى أيّ عنصر في هذا الصنف عن المستخدِمين المبصرين، وسيظل متاحًا لمستخدِمي قارئ الشاشة، لأن هذه الكلمات لا يحتاجها المستخدِمون المبصرون، إذ تُستخدَم لتقديم المزيد من المعلومات حول ما يفعله الزر لمستخدِمي قارئ الشاشة الذين ليس لديهم القدرة البصرية الإضافية لمساعدتهم، كما يمكنك العثور على العنصر <ul> التالي:

<ul
  role="list"
  className="todo-list stack-large stack-exception"
  aria-labelledby="list-heading"
>

تساعد السمة role التقنيات المساعِدة في توضيح نوع العنصر الذي يمثِّله الوسم، إذ يجري التعامل مع العنصر <ul> بوصفه قائمةً افتراضيًا، ولكن ستؤدي التنسيقات التي نضيفها إلى تعطيل هذه الوظيفة، في حين ستؤدي السمة role إلى استعادة القائمة التي تعني العنصر <ul>.

تخبر السمة aria-labelledby التقنيات المساعِدة بتعاملنا مع عنوان قائمتنا بوصفه العنوان الذي يصف الغرض من القائمة الموجودة تحته، مما يساعد مستخدِمي قارئ الشاشة على فهم الغرض منها بصورة أفضل، وأخيرًا، فتملك عناصر label وinput في عناصر القائمة بعض السمات الفريدة الخاصة بصيغة JSX، وهي:

<input id="todo-0" type="checkbox" defaultChecked={true} />
<label className="todo-label" htmlFor="todo-0">
  Eat
</label>

السمة defaultChecked في الوسم <input /‎> تخبر React بتحديد مربع الاختيار مبدئيًا، فإذا أردنا استخدام السمة checked كما نفعل في HTML، فستعرِض React بعض التحذيرات المتعلقة بمعالجة أحداث مربع الاختيار في طرفية متصفحك (نافذة console)، والتي يجب تجنبها، ولا تقلق كثيرًا بشأن ذلك في الوقت الحالي، إذ سنغطي ذلك لاحقًا عندما نبدأ باستخدام الأحداث.

تتوافق السمة htmlFor مع السمة for المُستخدَمة في لغة HTML، ولا يمكننا استخدام الكلمة for بوصفها سمةً في صيغة JSX لأنها كلمة محجوزة، لذلك تستخدِم React السمة htmlFor بدلًا من ذلك.

ملاحظتان:

  • يمكنك استخدام القيم المنطقية -أي true وfalse- في سمات JSX من خلال إحاطة هذه القيم بأقواس معقوصة، فإذا كتبت السمة defaultChecked="true"‎ مثلًا، فستكون "true" هي قيمة السمة defaultChecked، والتي تُعَدّ سلسلةً حرفيةً String Literal، لأنها لغة جافاسكربت وليست لغة HTML.
  • تملك السمة aria-pressed القيمة "true" في مثالنا لأنّ aria-pressed ليست سمةً منطقيةً حقيقيةً بالطريقة التي تستخدِمها السمة checked بها.

تنفيذ التنسيقات

الصق شيفرة CSS التالية في الملف src/index.css لتحُل محل ما هو موجود حاليًا:

/* إعادة الضبط */
*,
*::before,
*::after {
  box-sizing: border-box;
}
*:focus {
  outline: 3px dashed #228bec;
  outline-offset: 0;
}
html {
  font: 62.5% / 1.15 sans-serif;
}
h1,
h2 {
  margin-bottom: 0;
}
ul {
  list-style: none;
  padding: 0;
}
button {
  border: none;
  margin: 0;
  padding: 0;
  width: auto;
  overflow: visible;
  background: transparent;
  color: inherit;
  font: inherit;
  line-height: normal;
  -webkit-font-smoothing: inherit;
  -moz-osx-font-smoothing: inherit;
  -webkit-appearance: none;
}
button::-moz-focus-inner {
  border: 0;
}
button,
input,
optgroup,
select,
textarea {
  font-family: inherit;
  font-size: 100%;
  line-height: 1.15;
  margin: 0;
}
button,
input {
  overflow: visible;
}
input[type="text"] {
  border-radius: 0;
}
body {
  width: 100%;
  max-width: 68rem;
  margin: 0 auto;
  font: 1.6rem/1.25 Arial, sans-serif;
  background-color: #f5f5f5;
  color: #4d4d4d;
}
@media screen and (min-width: 620px) {
  body {
    font-size: 1.9rem;
    line-height: 1.31579;
  }
}
/* نهاية إعادة الضبط */
/* التنسيقات العامة */
.form-group > input[type="text"] {
  display: inline-block;
  margin-top: 0.4rem;
}
.btn {
  padding: 0.8rem 1rem 0.7rem;
  border: 0.2rem solid #4d4d4d;
  cursor: pointer;
  text-transform: capitalize;
}
.btn.toggle-btn {
  border-width: 1px;
  border-color: #d3d3d3;
}
.btn.toggle-btn[aria-pressed="true"] {
  text-decoration: underline;
  border-color: #4d4d4d;
}
.btn__danger {
  color: #fff;
  background-color: #ca3c3c;
  border-color: #bd2130;
}
.btn__filter {
  border-color: lightgrey;
}
.btn__primary {
  color: #fff;
  background-color: #000;
}
.btn-group {
  display: flex;
  justify-content: space-between;
}
.btn-group > * {
  flex: 1 1 49%;
}
.btn-group > * + * {
  margin-left: 0.8rem;
}
.label-wrapper {
  margin: 0;
  flex: 0 0 100%;
  text-align: center;
}
.visually-hidden {
  position: absolute !important;
  height: 1px;
  width: 1px;
  overflow: hidden;
  clip: rect(1px 1px 1px 1px);
  clip: rect(1px, 1px, 1px, 1px);
  white-space: nowrap;
}
[class*="stack"] > * {
  margin-top: 0;
  margin-bottom: 0;
}
.stack-small > * + * {
  margin-top: 1.25rem;
}
.stack-large > * + * {
  margin-top: 2.5rem;
}
@media screen and (min-width: 550px) {
  .stack-small > * + * {
    margin-top: 1.4rem;
  }
  .stack-large > * + * {
    margin-top: 2.8rem;
  }
}
.stack-exception {
  margin-top: 1.2rem;
}
/* نهاية التنسيقات العامة */
.todoapp {
  background: #fff;
  margin: 2rem 0 4rem 0;
  padding: 1rem;
  position: relative;
  box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 2.5rem 5rem 0 rgba(0, 0, 0, 0.1);
}
@media screen and (min-width: 550px) {
  .todoapp {
    padding: 4rem;
  }
}
.todoapp > * {
  max-width: 50rem;
  margin-left: auto;
  margin-right: auto;
}
.todoapp > form {
  max-width: 100%;
}
.todoapp > h1 {
  display: block;
  max-width: 100%;
  text-align: center;
  margin: 0;
  margin-bottom: 1rem;
}
.label__lg {
  line-height: 1.01567;
  font-weight: 300;
  padding: 0.8rem;
  margin-bottom: 1rem;
  text-align: center;
}
.input__lg {
  padding: 2rem;
  border: 2px solid #000;
}
.input__lg:focus {
  border-color: #4d4d4d;
  box-shadow: inset 0 0 0 2px;
}
[class*="__lg"] {
  display: inline-block;
  width: 100%;
  font-size: 1.9rem;
}
[class*="__lg"]:not(:last-child) {
  margin-bottom: 1rem;
}
@media screen and (min-width: 620px) {
  [class*="__lg"] {
    font-size: 2.4rem;
  }
}
.filters {
  width: 100%;
  margin: unset auto;
}
/* تنسيقات عناصر‫ Todo */
.todo {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
}
.todo > * {
  flex: 0 0 100%;
}
.todo-text {
  width: 100%;
  min-height: 4.4rem;
  padding: 0.4rem 0.8rem;
  border: 2px solid #565656;
}
.todo-text:focus {
  box-shadow: inset 0 0 0 2px;
}
/* تنسيقات مربعات الاختيار */
.c-cb {
  box-sizing: border-box;
  font-family: Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  font-weight: 400;
  font-size: 1.6rem;
  line-height: 1.25;
  display: block;
  position: relative;
  min-height: 44px;
  padding-left: 40px;
  clear: left;
}
.c-cb > label::before,
.c-cb > input[type="checkbox"] {
  box-sizing: border-box;
  top: -2px;
  left: -2px;
  width: 44px;
  height: 44px;
}
.c-cb > input[type="checkbox"] {
  -webkit-font-smoothing: antialiased;
  cursor: pointer;
  position: absolute;
  z-index: 1;
  margin: 0;
  opacity: 0;
}
.c-cb > label {
  font-size: inherit;
  font-family: inherit;
  line-height: inherit;
  display: inline-block;
  margin-bottom: 0;
  padding: 8px 15px 5px;
  cursor: pointer;
  touch-action: manipulation;
}
.c-cb > label::before {
  content: "";
  position: absolute;
  border: 2px solid currentColor;
  background: transparent;
}
.c-cb > input[type="checkbox"]:focus + label::before {
  border-width: 4px;
  outline: 3px dashed #228bec;
}
.c-cb > label::after {
  box-sizing: content-box;
  content: "";
  position: absolute;
  top: 11px;
  left: 9px;
  width: 18px;
  height: 7px;
  transform: rotate(-45deg);
  border: solid;
  border-width: 0 0 5px 5px;
  border-top-color: transparent;
  opacity: 0;
  background: transparent;
}
.c-cb > input[type="checkbox"]:checked + label::after {
  opacity: 1;
}

احفظ الملف وألقِ نظرةً على المتصفح، إذ يجب أن يتمتع تطبيقك الآن بتنسيق مقبول.

الخلاصة

يبدو تطبيق قائمة المهام الآن أشبه بتطبيق حقيقي، ولكنه لا يفعل أيّ شيء فعليًا، إذ سنبدأ في إصلاح ذلك لاحقًا في المقالات القادمة.

ترجمة -وبتصرُّف- للمقال Beginning our React todo list.

اقرأ أيضًا


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

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

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



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

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

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

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   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.


×
×
  • أضف...