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

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

  • المتطلبات الأساسية: يوصَى على الأقل بأن تكون على دراية بأساسيات لغات HTML وCSS وجافاسكربت JavaScript، ومعرفة باستخدام سطر الأوامر أو الطرفية، وستحتاج طرفية مثبَّت عليها node و npm لتصريف وبناء تطبيقك.
  • الهدف: تعلم كيفية تقسيم تطبيقنا إلى مكونات ومشاركة المعلومات فيما بينها.

يمكنك متابعة كتابة شيفرتك معنا، لذلك انسخ أولًا مستودع github -إذا لم تفعل ذلك مسبقًا- باستخدام الأمر التالي:

git clone https://github.com/opensas/mdn-svelte-tutorial.git

ثم يمكنك الوصول إلى حالة التطبيق الحالية من خلال تشغيل الأمر التالي:

cd mdn-svelte-tutorial/04-componentizing-our-app

أو يمكنك تنزيل محتوى المجلد مباشرةً كما يلي:

npx degit opensas/mdn-svelte-tutorial/04-componentizing-our-app

تذكَّر تشغيل الأمر npm install && npm run dev لبدء تشغيل تطبيقك في وضع التطوير، وإذا أردت متابعتنا فابدأ بكتابة الشيفرة باستخدام الأداة REPL.

تقسيم التطبيق إلى مكونات

يتكون التطبيق في إطار عمل Svelte من مكوِّن واحد أو من مكونات متعددة، ويُعَدّ المكوِّن كتلةً من الشيفرة البرمجية القابلة لإعادة الاستخدام والمستقلة ذاتيًا والتي تغلّف شيفرة HTML و CSS وجافاسكربت المرتبطة مع بعضها البعض والمكتوبة في ملف ‎.svelte، كما يمكن أن تكون المكونات كبيرةً أو صغيرةً، لكنها تكون عادةً محدَّدةً بوضوح، فالمكونات الأكثر فاعليةً هي المكونات التي تخدم غرضًا واحدًا واضحًا.

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

لا توجد قواعد صارمة لتقسيم المكونات، لذا يفضِّل بعض الأشخاص اتباع نهج بسيط يتمثل بالنظر إلى شيفرة HTML ثم رسم مربعات حول كل مكوِّن ومكوِّن فرعي يبدو أنّ له شيفرته الخاصة، في حين يطبق أشخاص آخرون الأساليب نفسها المُستخدَمة لتحديد ما إذا كان يجب إنشاء دالة أو كائن جديد، وأحد هذه الأساليب هو مبدأ المسؤولية الفردية، أي يجب أن يطبّق المكون شيئًا واحدًا فقط بصورة مثالية، ثم يمكننا تقسيمه إلى مكونات فرعية أصغر إذا لزم الأمر، كما يجب أن يكمل هذا النهجان بعضهما البعض لمساعدتك على تحديد كيفية تنظيم مكوناتك بطريقة أفضل.

سنقسم تطبيقنا إلى المكونات التالية:

  • Alert.svelte: مربع إشعارات عام لإرسال الإجراءات التي حدثت.
  • NewTodo.svelte: حقل إدخال النص والزر الذي يسمح بإدخال عنصر مهام جديد.
  • FilterButton.svelte: أزرار "كل المهام All" و"المهام النشطة Active" و"المهام المكتملة Completed" التي تسمح بتطبيق المرشّحات Filters على عناصر المهام المعروضة.
  • TodosStatus.svelte: العنوان الذي يعرض العبارة "x out of y items completed" التي تمثّل عدد المهام المكتملة.
  • Todo.svelte: عنصر مهام مفرد، إذ سيُعرَض كل عنصر مهمة مرئي في نسخة منفصلة من هذا المكوِّن.
  • MoreActions.svelte: الزرّان "تحديد الكل Check All" و"احذف المهام المكتملة Remove Completed" الموجودان أسفل واجهة المستخدِم، ويسمحان بتنفيذ مجموعة إجراءات على عناصر المهام.

تقسيم التطبيق إلى مكونات

سنركز في هذا المقال على إنشاء المكونين FilterButton و Todo وسنشرح المكونات الأخرى في المقالات القادمة.

ملاحظة: سنتعلم أيضًا في عملية إنشاء أول مكونين تقنيات مختلفة لتواصل المكونات مع بعضها بعضًا، وإيجابيات وسلبيات كل من هذه التقنيات.

استخراج مكون الترشيح

سننشئ أولًا المكون FilterButton.svelte باتباع الخطوات التالية:

أولًا، أنشئ ملفًا جديدًا components/FilterButton.svelte.

ثانيًا، سنصرّح عن الخاصية filter في هذا الملف ثم سننسخ شيفرة HTML المتعلقة به من الملف Todos.svelte، لذا أضِف المحتوى التالي إلى هذا الملف:

<script>
  export let filter = 'all'
</script>

<div class="filters btn-group stack-exception">
  <button class="btn toggle-btn" class:btn__primary={filter === 'all'} aria-pressed={filter === 'all'} on:click={()=> filter = 'all'} >
    <span class="visually-hidden">Show</span>
    <span>All</span>
    <span class="visually-hidden">tasks</span>
  </button>
  <button class="btn toggle-btn" class:btn__primary={filter === 'active'} aria-pressed={filter === 'active'} on:click={()=> filter = 'active'} >
    <span class="visually-hidden">Show</span>
    <span>Active</span>
    <span class="visually-hidden">tasks</span>
  </button>
  <button class="btn toggle-btn" class:btn__primary={filter === 'completed'} aria-pressed={filter === 'completed'} on:click={()=> filter = 'completed'} >
    <span class="visually-hidden">Show</span>
    <span>Completed</span>
    <span class="visually-hidden">tasks</span>
  </button>
</div>

ثالثًا، ارجع إلى المكوِّن Todos.svelte، حيث نريد الاستفادة من المكوِّن FilterButton، إذ يجب استيراده أولًا، لذا أضِف السطر التالي قبل القسم <script> في المكوِّن Todos.svelte:

import FilterButton from './FilterButton.svelte'

رابعًا، استبدل الآن العنصر <div> الذي يملك اسم الصنف filters باستدعاء المكون FilterButton الذي يأخذ المرشح الحالي بوصفه خاصيةً كما يلي:

<FilterButton {filter} />

ملاحظة: تذكَّر أنه إذا تطابق اسم سمة في لغة HTML مع اسم المتغير، فيمكن استبدالهما بالشكل {variable}، لذا يمكننا استبدال <FilterButton filter={filter} /‎> بالشكل <FilterButton {filter} /‎>.

لنجرب التطبيق الآن، حيث ستلاحظ أنه إذا نقرت على أزرار الترشيح، فستُحدَّد هذه الأزرار وسيُحدَّث التنسيق بطريقة مناسبة، ولكن لدينا مشكلة وهي عدم ترشيح المهام، وسبب هذه المشكلة هو انتقال المتغير filter من المكوِّن Todos إلى المكوِّن FilterButton عبر الخاصية، ولكن لا تنتقل التغييرات التي تحدث في المكوِّن FilterButton مرةً أخرى إلى المكوِّن الأب، إذ يكون ارتباط البيانات أحادي الاتجاه افتراضيًا.

مشاركة البيانات بين المكونات: تمرير المعالج بوصفه خاصية

تتمثل إحدى طرق السماح للمكونات الأبناء بإعلام المكونات الآباء بأيّ تغييرات في تمرير المعالج بوصفه خاصيةً Prop، حيث سينفّذ المكوِّن الابن المعالج، ويمرّر المعلومات المطلوبة بوصفها معاملًا وسيعدّل المعالج حالة المكوِّن الأب، كما سيتلقّى المكوِّن FilterButton في حالتنا المعالج onclick من المكوِّن الأب، فإذا نقر المستخدِم على أيّ زر ترشيح، فسيستدعي المكونُ الابن المعالجَ onclick ويمرّر المرشّح المحدد على أساس معامل إلى المكوِّن الأب.

سنصرّح فقط عن الخاصية onclick التي تُسنَد إلى معالِج وهمي لمنع الأخطاء كما يلي:

export let onclick = (clicked) => {}

وسنصرّح عن التعليمة التفاعلية ‎$: onclick(filter)‎ لاستدعاء المعالِج onclick كلما جرى تحديث المتغير filter.

أولًا، يجب أن يبدو القسم <script> الخاص بالمكوِّن FilterButton كما يلي:

<script>
  export let filter = 'all'
  export let onclick = (clicked) => {}
  $: onclick(filter)
</script>

إذا استدعينا المكوِّن FilterButton ضمن المكوِّن Todos.svelte الآن، فيجب تحديد المعالج، لذا عدّله إلى ما يلي:

<FilterButton {filter} onclick={ (clicked) => filter = clicked }/>

إذا نقرت على أيّ زر ترشيح، فسنعدّل المتغير filter باستخدام المرشّح الجديد وسيعمل المكوِّن FilterButton مرةً أخرى.

طريقة أسهل لربط البيانات ثنائي الاتجاه باستخدام الموجه bind

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

يعمل التطبيق جيدًا، ولكن يوفر إطار عمل Svelte طريقةً سهلةً ومباشرةً لتحقيق ربط البيانات ثنائي الاتجاه، إذ تتدفق البيانات عادةً من المكوِّن الأب إلى المكوِّن الابن باستخدام الخاصيات، وإذا أردنا أن تتدفق في الاتجاه الآخر من المكوِّن الابن إلى المكوِّن الأب، فيمكننا استخدام الموجّه bind:‎.

سنخبر إطار عمل Svelte باستخدام الموجّه bind أنّ أيّ تغييرات تجرَى على الخاصية filter في المكوِّن FilterButton يجب أن تنتشر إلى المكوِّن الأب Todos، أي أننا سنربط قيمة المتغير filter في المكوِّن الأب بقيمته في المكوِّن الابن.

أولًا، عدّل استدعاء المكوِّن FilterButton في Todos.svelte كما يلي:

<FilterButton bind:filter={filter} />

يوفِّر إطار عمل Svelte اختصارًا، إذ تعادل التعليمةُ bind:value={value}‎ التعليمةَ bind:value، لذلك يمكنك في المثال السابق كتابة <FilterButton bind:filter /‎> فقط.

ثانيًا، يمكن للمكوِّن الابن الآن تعديل قيمة المتغير filter الخاص بالمكون الأب، لذلك لم نعد بحاجة إلى الخاصية onclick، لذا عدّل القسم <script> الخاص بالمكوِّن FilterButton كما يلي:

<script>
  export let filter = 'all'
</script>

ثالثًا، جرب تطبيقك مرةً أخرى، وستظل ترى أن المرشّحات تعمل بصورة صحيحة.

إنشاء المكون Todo

سننشئ الآن المكون Todo لتغليف كل مهمة بما في ذلك مربع الاختيار وشيفرة التعديل لتتمكّن من تعديل مهمة موجودة مسبقًا، وسيتلقى المكوِّن Todo الكائن todo بوصفه خاصيةً، لذا لنصرّح عن الخاصية todo ولننقل الشيفرة البرمجية من المكوِّن Todos، كما سنستبدل حاليًا استدعاء removeTodo باستدعاء alert وسنضيف هذه الوظيفة مرةً أخرى في وقت لاحق.

أنشئ ملف مكوِّن جديد components/Todo.svelte، وضَع بعد ذلك المحتويات التالية ضمن هذا الملف:

<script>
  export let todo
</script>

<div class="stack-small">
  <div class="c-cb">
    <input type="checkbox" id="todo-{todo.id}"
      on:click={() => todo.completed = !todo.completed}
      checked={todo.completed}
    />
    <label for="todo-{todo.id}" class="todo-label">{todo.name}</label>
  </div>
  <div class="btn-group">
    <button type="button" class="btn">
      Edit <span class="visually-hidden">{todo.name}</span>
    </button>
    <button type="button" class="btn btn__danger" on:click={() => alert('not implemented')}>
      Delete <span class="visually-hidden">{todo.name}</span>
    </button>
  </div>
</div>

يجب الآن استيراد المكوِّن Todo إلى Todos.svelte، لذا انتقل إلى هذا الملف الآن وأضف تعليمة الاستيراد import التالية بعد تعليمة الاستيراد الموجودة مسبقًا:

import Todo from './Todo.svelte'

يجب بعد ذلك تحديث كتلة {‎#each} لتضمين المكوِّن <Todo> لكل مهمة بدلًا من الشيفرة المنقولة إلى Todo.svelte، ويجب تمرير كائن todo الحالي إلى المكوِّن بوصفه خاصيةً، لذا عدّل كتلة {‎#each} ضمن المكوِّن Todos.svelte كما يلي:

<ul role="list" class="todo-list stack-large" aria-labelledby="list-heading">
  {#each filterTodos(filter, todos) as todo (todo.id)}
    <li class="todo">
      <Todo {todo} />
    </li>
  {:else}
    <li>Nothing to do here!</li>
  {/each}
</ul>

تُعرَض قائمة المهام على الصفحة، ويجب أن تعمل مربعات الاختيار (حاول تحديد أو إلغاء تحديد مربعات الاختيار، ثم لاحظ أنّ المرشحات لا تزال تعمل كما هو متوقع)، ولكن لن يُحدَّث عنوان الحالة "x out of y items completed" وفقًا لذلك لأن المكوِّن Todo يتلقى المهام باستخدام الخاصية، لكنه لا يرسل أيّ معلومات إلى المكوِّن الأب، وسنصلح ذلك لاحقًا.

مشاركة البيانات بين المكونات: نمط الخاصيات للأسفل Props-down والأحداث للأعلى Events-up

يُعَد الموجّه bind واضحًا جدًا ويسمح بمشاركة البيانات بين المكوِّن الأب والمكوِّن الابن، ولكن يمكن أن يكون تتبّع جميع القيم المرتبطة ببعضها بعضًا أمرًا صعبًا عندما ينمو تطبيقك بصورة أكبر وأكثر تعقيدًا، لذا يمكنك استخدام نهج مختلف هو نمط الاتصال "props-down, events-up".

يعتمد هذا النمط على المكونات الأبناء التي تتلقى البيانات من آبائها عبر الخاصيات والمكونات الآباء لتحديث حالتها من خلال معالجة الأحداث التي تطلقها المكونات الأبناء، لذا تتدفق الخاصيات للأسفل Flow Down من المكوِّن الأب إلى المكوِّن الابن وتنتشر Bubble Up الأحداث للأعلى من المكوِّن الابن إلى المكوِّن الأب، إذ ينشئ هذا النمط تدفقًا أسهل ثنائي الاتجاه للمعلومات.

لنلقِ نظرةً على كيفية إصدار أحداثنا لإعادة تطبيق وظيفة زر "الحذف Delete" المفقودة، إذ يمكن إنشاء أحداث مخصصة من خلال استخدام الأداة createEventDispatcher التي تعيد الدالة dispatch()‎ التي تسمح بإصدار أحداث مخصصة، فإذا أرسلتَ حدثًا، فيجب تمرير اسم الحدث وكائن اختياري به معلومات إضافية تريد تمريرها إلى كل مستمع، كما ستكون هذه البيانات الإضافية متاحةً في الخاصية detail لكائن الحدث.

ملاحظة: تشترك الأحداث المخصصة في إطار عمل Svelte بواجهة برمجة التطبيقات نفسها التي تستخدِمها أحداث DOM العادية، كما يمكنك نشر حدث إلى المكوِّن الأب عن طريق تحديد on:event بدونّ أي معالج.

سنعدّل المكون Todo لإصدار الحدث remove عبر تمرير المهمة المحذوفة بوصفها معلومات إضافية.

أضِف أولًا الأسطر التالية إلى الجزء العلوي من القسم <script> للمكوِّن Todo:

import { createEventDispatcher } from 'svelte'
const dispatch = createEventDispatcher()

عدّل الآن زر "الحذف Delete" في قسم شيفرة HTML بالملف نفسه ليبدو كما يلي:

<button type="button" class="btn btn__danger" on:click={() => dispatch('remove', todo)}>
  Delete <span class="visually-hidden">{todo.name}</span>
</button>

نصدر الحدث remove من خلال استخدام dispatch('remove', todo)‎ ونمرِّر المهام todo المحذوفة بوصفها بيانات إضافية، إذ سيُستدعى المعالج باستخدام كائن الحدث المتوفر مع البيانات الإضافية المتوفرة في الخاصية event.detail.

يجب الآن الاستماع إلى هذا الحدث من داخل الملف Todos.svelte والتصرف وفقًا لذلك، لذا ارجع إلى هذا الملف وعدّل استدعاء المكوِّن <Todo> كما يلي:

<Todo {todo} on:remove={e => removeTodo(e.detail)} />

يتلقى معالجنا المعامِل e (كائن الحدث) الذي يحتفظ بالمهام المحذوفة في الخاصية detail.

إذا حاولت تجربة تطبيقك مرةً أخرى الآن، فسترى أنّ وظيفة الحذف تعود للعمل، وبذلك نجح حدثنا المخصّص كما توقعنا، كما يرسل مستمع الحدث remove تغيّر البيانات إلى المكوِّن الأب، لذلك سيُحدَّث عنوان الحالة "x out of y items completed" بصورة مناسبة عند حذف المهام.

سنهتم الآن بالحدث update بحيث يمكن إعلام المكوِّن الأب بأيّ مهام مُعدَّلة.

تحديث المهام

لا يزال يتعين علينا تنفيذ الوظيفة للسماح بتعديل المهام الحالية، إذ يجب تضمين وضع التعديل في المكوِّن Todo، كما سنعرض حقل الإدخال <input> عند الدخول في وضع التعديل للسماح بتعديل اسم المهمة الحالي مع زرين لتأكيد التغييرات أو إلغائها.

معالجة الأحداث

أولًا، سنحتاج متغيرًا واحدًا لتتبّع ما إذا كنا في وضع التعديل أم في وضع آخر لتخزين اسم المهمة المُعدَّلة، لذا أضِف تعريفات المتغيرات التالية في الجزء السفلي من القسم <script> للمكوِّن Todo:

let editing = false                     // تتبّع نمط التعديل
let name = todo.name                    // تخزين اسم المهمة المُعدَّلة

يجب أن نقرِّر ما هي الأحداث التي سيصدرها المكوِّن Todo كما يلي:

  • يمكننا إصدار أحداث مختلفة لتبديل الحالة وتعديل الاسم مثل updateTodoStatus و updateTodoName.
  • أو يمكننا اتباع نهج أعم وإصدار حدث update واحد لكلتا العمليتين.

سنتخذ النهج الثاني لنتمكن من إظهار طريقة مختلفة، إذ تتمثل ميزة هذا النهج في أنه يمكننا لاحقًا إضافة المزيد من الحقول إلى المهام مع إمكانية معالجة جميع التحديثات باستخدام الحدث نفسه، فلننشئ الدالة update()‎ التي ستتلقى التغييرات وتصدر حدث تحديث مع المهام المُعدَّلة، لذا أضف ما يلي مرةً أخرى إلى الجزء السفلي من القسم <script>:

function update(updatedTodo) {
  todo = { ...todo, ...updatedTodo }    // تطبيق تعديلات على المهمة
  dispatch('update', todo)              // إصدار حدث التحديث
}

استخدمنا صيغة الانتشار Spread Syntax لإعادة المهمة الأصلية مع التعديلات المُطبَّقة عليها.

سننشئ بعد ذلك دوالًا مختلفةً للتعامل مع كل إجراء للمستخدِم، إذ يمكن للمستخدِم حفظ التغييرات أو إلغائها عندما تكون المهمة في وضع التعديل، ويمكن للمستخدِم حذف المهمة أو تعديلها أو تبديل حالتها بين الحالة المكتملة والنشطة عندما لا تكون في وضع التعديل، لذا أضف مجموعة الدوال التالية بعد آخر دالة للتعامل مع هذه الإجراءات:

function onCancel() {
  name = todo.name                      // ‫إعادة المتغير name إلى قيمته الأولية 
  editing = false                       // والخروج من وضع التعديل
}

function onSave() {
  update({ name: name })                // تحديث اسم المهمة
  editing = false                       // والخروج من وضع التعديل
}

function onRemove() {
  dispatch('remove', todo)              // إصدار حدث الحذف
}

function onEdit() {
  editing = true                        // الدخول في وضع التعديل
}

function onToggle() {
  update({ completed: !todo.completed}) // تحديث حالة المهمة
}

تحديث ملف شيفرة HTML

يجب الآن تحديث شيفرة HTML الخاصة بالمكون Todo لاستدعاء الدوال السابقة عند اتخاذ الإجراءات المناسبة، إذ يمكنك التعامل مع وضع التعديل من خلال استخدام المتغير editing الذي له قيمة منطقية، فإذا كانت قيمة هذا المتغير true، فيجب أن يُعرَض حقل الإدخال <input> لتعديل اسم المهمة وزرَّي "الإلغاء Cancel" و"الحفظ Save"؛ أما إذا لم تكن في وضع التعديل، فسيُعرَض مربع الاختيار واسم المهمة وأزرار تعديل المهام وحذفها.

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

إذا كانت قيمة المتغير editing هي true مثلًا، فسيعرض إطار عمل Svelte نموذج التحديث؛ أما إذا كانت قيمته false، فسيزيله من نموذج DOM وسيضيف مربع الاختيار، لذا سيكون تعيين قيمة المتغير editing كافيًا لعرض عناصر HTML الصحيحة بفضل خاصية التفاعل في إطار عمل Svelte.

ستكون كتلة if كما يلي:

<div class="stack-small">
{#if editing}
  <!-- markup for editing to-do: label, input text, Cancel and Save Button -->
{:else}
  <!-- markup for displaying to-do: checkbox, label, Edit and Delete Button -->
{/if}
</div>

يمثِّل الجزء {‎:else} أو النصف السفلي من كتلة if قسم عدم التعديل، كما سيكون مشابهًا جدًا للقسم الموجود في المكوِّن Todos، ولكن الاختلاف الوحيد بينهما هو أننا نستدعي الدوال onToggle()‎ و onEdit()‎ و onRemove()‎ اعتمادًا على إجراء المستخدِم.

{:else}
  <div class="c-cb">
    <input type="checkbox" id="todo-{todo.id}"
      on:click={onToggle} checked={todo.completed}
    >
    <label for="todo-{todo.id}" class="todo-label">{todo.name}</label>
  </div>
  <div class="btn-group">
    <button type="button" class="btn" on:click={onEdit}>
      Edit<span class="visually-hidden"> {todo.name}</span>
    </button>
    <button type="button" class="btn btn__danger" on:click={onRemove}>
      Delete<span class="visually-hidden"> {todo.name}</span>
    </button>
  </div>
{/if}
</div>

تجدر الإشارة إلى ما يلي:

  • ننفّذ الدالة onEdit()‎ التي تضبط المتغير editing على القيمة true عندما يضغط المستخدِم على زر "التعديل Edit".
  • نستدعي الدالة onToggle()‎ التي تنفذ الدالة update()‎ من خلال تمرير كائن مع قيمة completed الجديدة بوصفه معامِلًا عندما ينقر المستخدِم على مربع الاختيار.
  • تصدِر الدالة update()‎ الحدث update من خلال تمرير نسخة من المهمة الأصلية مع التغييرات المطبَّقة بوصفها معلومات إضافية.
  • أخيرًا، تصدِر الدالة onRemove()‎ الحدث remove من خلال تمرير المهمة todo المراد حذفها بوصفها بيانات إضافية.

ستحتوي واجهة المستخدِم الخاصة بالتعديل -أي النصف العلوي- على حقل الإدخال <input> وزرين لإلغاء التغييرات أو حفظها كما يلي:

<div class="stack-small">
{#if editing}
  <form on:submit|preventDefault={onSave} class="stack-small" on:keydown={e => e.key === 'Escape' && onCancel()}>
    <div class="form-group">
      <label for="todo-{todo.id}" class="todo-label">New name for '{todo.name}'</label>
      <input bind:value={name} type="text" id="todo-{todo.id}" autoComplete="off" class="todo-text" />
    </div>
    <div class="btn-group">
      <button class="btn todo-cancel" on:click={onCancel} type="button">
        Cancel<span class="visually-hidden">renaming {todo.name}</span>
        </button>
      <button class="btn btn__primary todo-edit" type="submit" disabled={!name}>
        Save<span class="visually-hidden">new name for {todo.name}</span>
      </button>
    </div>
  </form>
{:else}
[...]

إذا ضغط المستخدِم على زر "التعديل Edit"، فسيُضبَط المتغير editing على القيمة true وسيزيل إطار عمل Svelte شيفرة HTML في الجزء {‎:else} من نموذج DOM وسيستبدله بشيفرة HTML الموجودة في القسم {‎#if‎}.

ستكون الخاصية value الخاصة بالعنصر <input> مرتبطةً بالمتغير name، وستستدعي أزرار إلغاء التغييرات وحفظها الدالتين onCancel()‎ و onSave()‎ على التوالي كما يلي، وقد أضفنا هاتين الدالتين سابقًا:

  • إذا استُدعيت الدالة onCancel()‎، فستُعاد الخاصية name إلى قيمتها الأصلية عند تمريرها بوصفها خاصيةً Prop وسنخرج من وضع التعديل عن طريق ضبط المتغير editing على القيمة false.
  • إذا استُدعيت الدالة onSave()‎، فسنشغّل الدالة update()‎ من خلال تمرير الخاصية name المُعدَّلة، وسنخرج من وضع التعديل.

كما نعطّل زر "الحفظ Save" عندما يكون حقل الإدخال <input> فارغًا باستخدام السمة disabled={!name}‎، كما نسمح للمستخدِم بإلغاء التعديل باستخدام المفتاح Escape كما يلي:

on:keydown={e => e.key === 'Escape' && onCancel()}.

كما نستخدِم الخاصية todo.id لإنشاء معرّفات فريدة لعناصر التحكم بحقل الإدخال والتسميات Labels الجديدة.

تبدو شيفرة HTML المعدَّلة الكاملة للمكون Todo كما يلي:

<div class="stack-small">
{#if editing}
  <!-- markup for editing todo: label, input text, Cancel and Save Button -->
  <form on:submit|preventDefault={onSave} class="stack-small" on:keydown={e => e.key === 'Escape' && onCancel()}>
    <div class="form-group">
      <label for="todo-{todo.id}" class="todo-label">New name for '{todo.name}'</label>
      <input bind:value={name} type="text" id="todo-{todo.id}" autoComplete="off" class="todo-text" />
    </div>
    <div class="btn-group">
      <button class="btn todo-cancel" on:click={onCancel} type="button">
        Cancel<span class="visually-hidden">renaming {todo.name}</span>
        </button>
      <button class="btn btn__primary todo-edit" type="submit" disabled={!name}>
        Save<span class="visually-hidden">new name for {todo.name}</span>
      </button>
    </div>
  </form>
{:else}
  <!-- markup for displaying todo: checkbox, label, Edit and Delete Button -->
  <div class="c-cb">
    <input type="checkbox" id="todo-{todo.id}"
      on:click={onToggle} checked={todo.completed}
    >
    <label for="todo-{todo.id}" class="todo-label">{todo.name}</label>
  </div>
  <div class="btn-group">
    <button type="button" class="btn" on:click={onEdit}>
      Edit<span class="visually-hidden"> {todo.name}</span>
    </button>
    <button type="button" class="btn btn__danger" on:click={onRemove}>
      Delete<span class="visually-hidden"> {todo.name}</span>
    </button>
  </div>
{/if}
</div>

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

يجب معالجة الحدث update من المكوِّن Todos لتشغيل وظيفة التحديث، لذا أضف معالِج الأحداث التالي في القسم <script>:

function updateTodo(todo) {
  const i = todos.findIndex(t => t.id === todo.id)
  todos[i] = { ...todos[i], ...todo }
}

نجد المهمة todo باستخدام معرِّفها id في مصفوفة المهام todos ونحدّث محتواها باستخدام صيغة الانتشار، وقد كان بإمكاننا أيضًا استخدام todos[i‎] = todo في هذه الحالة، ولكن هذا التطبيق أفضل، مما يسمح للمكوِّن Todo بإعادة الأجزاء المُعدَّلة فقط من المهام.

يجب بعد ذلك الاستماع إلى الحدث update في استدعاء المكون <Todo>، وتشغيل الدالة updateTodo()‎ عند حدوث ذلك لتغيير المتغير name والحالة completed، لذا عدّل استدعاء المكوِّن <Todo> كما يلي:

{#each filterTodos(filter, todos) as todo (todo.id)}
  <li class="todo">
    <Todo {todo}
      on:update={e => updateTodo(e.detail)}
      on:remove={e => removeTodo(e.detail)}
    />
  </li>

جرب تطبيقك مرةً أخرى وسترى أنه يمكنك حذف وإضافة وتعديل وإلغاء تعديل وتبديل حالة اكتمال المهام، وسيُعدَّل عنوان الحالة "x out of y items completed" بطريقة مناسبة عند اكتمال المهام.

يُعَدّ تطبيق نمط "props-down, events-up" في إطار عمل Svelte سهلًا، ولكن يمكن أن يكون الموجّه bind اختيارًا جيدًا للمكونات البسيطة، وسيتيح لك إطار Svelte الاختيار.

ملاحظة: يوفِّر إطار Svelte آليات أكثر تقدمًا لمشاركة المعلومات بين المكونات، وهي واجهة Context API والمخازن Stores، إذ توفِّر Context API آليةً للمكونات وأحفادها للتواصل مع بعضها البعض دون تمرير البيانات والدوال بوصفها خاصيات، أو إرسال الكثير من الأحداث، في حين تتيح المخازن Stores مشاركة البيانات التفاعلية بين المكونات غير المرتبطة بطريقة هرمية.

يمكنك الوصول إلى نسختك من مستودعنا على النحو التالي لمعرفة حالة الشيفرة كما يجب أن تكون في نهاية هذا المقال:

cd mdn-svelte-tutorial/05-advanced-concepts

أو يمكنك تنزيل محتوى المجلد مباشرةً باستخدام الأمر التالي:

npx degit opensas/mdn-svelte-tutorial/05-advanced-concepts

تذكر تشغيل الأمر npm install && npm run dev لبدء تشغيل تطبيقك في وضع التطوير، فإذا أردت متابعتنا فابدأ بكتابة الشيفرة باستخدام الأداة REPL.

الخلاصة

أضفنا جميع الوظائف المطلوبة لتطبيقنا، إذ يمكننا عرض المهام وإضافتها وتعديلها وحذفها وتمييزها على أنها مكتملة وترشيحها حسب الحالة، كما غطينا في هذا المقال المواضيع التالية:

  • استخراج وظائف مكوِّن جديد.
  • تمرير المعلومات من المكوِّن الابن إلى المكوِّن الأب باستخدام معالج يُستقبَل بوصفه خاصيةً.
  • تمرير المعلومات من المكوِّن الابن إلى المكوِّن الأب باستخدام الموجّه bind.
  • عرض كتل شيفرة HTML المشروطة باستخدام كتلة if.
  • تطبيق نمط الاتصال "props-down, events-up".
  • إنشاء الأحداث المخصصة والاستماع إليها.

سنواصل في المقال التالي من جزئية svelte من هذه السلسلة تقسيم تطبيقنا إلى مكونات ونتعرف على بعض التقنيات المتقدمة للعمل مع نموذج DOM.

ترجمة -وبتصرُّف- للمقال Componentizing our Svelte app.

اقرأ أيضًا


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

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

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



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

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

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

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


×
×
  • أضف...