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

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

  • المتطلبات الأساسية: يوصَى على الأقل بأن تكون على دراية بأساسيات لغات HTML وCSS وجافاسكربت JavaScript، ومعرفة باستخدام سطر الأوامر أو الطرفية.
  • الهدف: معرفة المزيد حول المكونات بما في ذلك كيفية عمل الأحداث للتعامل مع التحديثات، وإضافة وظائف تحديد المهام وتعديلها وحذفها.

إنشاء مكون جديد

أنشئ مكونًا بالاسم item باستخدام أمر واجهة سطر الأوامر CLI التالي:

ng generate component item

ينشئ الأمر ng generate component مكونًا ومجلدًا بالاسم الذي تحدده، حيث سيكون في مثالنا اسم المجلد والمكوِّن هو item، كما يمكنك العثور على مجلد العناصر item ضمن المجلد app.

يتكون المكوِّن ItemComponent من الملفات التالية مثل المكون AppComponent:

  • الملف item.component.html الخاص بالتوصيف باستخدام لغة HTML.
  • الملف item.component.ts الخاص بالشيفرة البرمجية أو بالمنطق باستخدام لغة TypeScript +الملف item.component.css الخاص بالتنسيق باستخدام لغة CSS.

يمكنك رؤية مرجع لملفات HTML وCSS في البيانات الوصفية لعنصر التصميم ‎@Component()‎ في الملف item.component.ts كما يلي:

@Component({
  selector: 'app-item',
  templateUrl: './item.component.html',
  styleUrls: ['./item.component.css'],
})

إضافة توصيف HTML الخاص بالمكون ItemComponent

يمكن أن يتولى المكوِّن ItemComponent مهمة منح المستخدِم طريقة لتحديد العناصر بوصفها مكتملةً أو لتعديلها أو حذفها، لذا أضِف شيفرة HTML لإدارة العناصر من خلال استبدال محتوى العنصر البديل Placeholder في الملف item.component.html بما يلي:

<div class="item">

  <input [id]="item.description" type="checkbox" (change)="item.done = !item.done" [checked]="item.done" />
  <label [for]="item.description">{{item.description}}</label>

  <div class="btn-wrapper" *ngIf="!editable">
    <button class="btn" (click)="editable = !editable">Edit</button>
    <button class="btn btn-warn" (click)="remove.emit()">Delete</button>
  </div>

  ‏<-- ‫يظهر هذا القسم فقط في حالة نقر المستخدم على زر "التعديل Edit" –!>
  <div *ngIf="editable">
    <input class="sm-text-input" placeholder="edit item" [value]="item.description" #editedItem (keyup.enter)="saveItem(editedItem.value)">

    <div class="btn-wrapper">
      <button class="btn" (click)="editable = !editable">Cancel</button>
      <button class="btn btn-save" (click)="saveItem(editedItem.value)">Save</button>
    </div>
  </div>

</div>

حقل الإدخال Input الأول هو مربع اختيار بحيث يمكن للمستخدِمين تحديد العناصر عند اكتمال أحدها، كما تشير الأقواس المزدوجة المعقوصة {{}} في حقل الإدخال <input> والتسمية <label> الخاصين بمربع الاختيار إلى استخدام إطار عمل Angular الذي يستخدِم الصيغة {{item.description}} لاسترداد وصف العنصر item الحالي من مصفوفة العناصر items، وسنوضّح في القسم التالي كيفية مشاركة المكونات للبيانات بالتفصيل.

كما يوجد زران لتعديل العنصر الحالي وحذفه ضمن الوسم <div> الذي يوجد فيه الموجّه ‎*ngIf، وهو موجّه Angular مبني مسبقًا يمكنك استخدامه لتغيير بنية نموذج DOM ديناميكيًا، إذ يشير الموجّه ‎*ngIf إلى أنه إذا كانت قيمة المتغير editable هي false، فإنّ العنصر <div> موجود في نموذج DOM؛ وإذا كانت قيمة المتغير editable هي true، فسيزيل إطار عمل Angular العنصر <div> من نموذج DOM.

<div class="btn-wrapper" *ngIf="!editable">
  <button class="btn" (click)="editable = !editable">Edit</button>
  <button class="btn btn-warn" (click)="remove.emit()">Delete</button>
</div>

إذا نقر المستخدِم على زر "التعديل Edit"، فستصبح قيمة المتغير editable هي true، مما يؤدي إلى إزالة العنصر <div> وأبنائه من نموذج DOM، فإذا نقر المستخدِم على زر "الحذف Delete"، فسيرفع المكوِّن ItemComponent حدثًا يعلِم فيه المكوِّن AppComponent بعملية الحذف.

لاحظ أيضًا وجود الموجه ‎*ngIf في عنصر <div> التالي، ولكن ضُبِط المتغير editable على القيمة true، فإذا كانت قيمة المتغير editable هي true في هذه الحالة، فسيضع إطار عمل Angular العنصر <div> وعناصره الأبناء <input> و <button> في نموذج DOM.

‏<-- ‫يظهر هذا القسم فقط في حالة نقر المستخدم على زر "التعديل Edit" –!>
<div *ngIf="editable">
  <input class="sm-text-input" placeholder="edit item" [value]="item.description" #editedItem (keyup.enter)="saveItem(editedItem.value)">

  <div class="btn-wrapper">
    <button class="btn" (click)="editable = !editable">Cancel</button>
    <button class="btn btn-save" (click)="saveItem(editedItem.value)">Save</button>
  </div>
</div>

تكون قيمة العنصر <input> مرتبطةً بوصف description العنصر الحالي باستخدام الصيغة ‎[value]="item.description"‎، وبالتالي يكون وصف description العنصر هو قيمة العنصر <input>، لذا إذا كانت قيمة الوصف description هي eat، فسيكون الوصف description موجودًا مسبفًا في العنصر <input>، وبذلك إذا عدّل المستخدِم العنصر، فستكون قيمة العنصر <input> هي eat مسبقًا.

يعني متغير القالب ‎#editedItem في العنصر <input> أنّ إطار عمل Angular يخزّن كل ما يكتبه المستخدِم في العنصر <input> في متغير اسمه editedItem، إذ يستدعي الحدث keyup التابع saveItem()‎ ويمرّر قيمة المتغير editedItem إذا اختار المستخدِم الضغط على مفتاح Enter بدلًا من النقر على زر "الحفظ Save".

إذا نقر المستخدِم على زر "الإلغاء Cancel"، فستتبدّل قيمة المتغير editable إلى false، مما يزيل حقل الإدخال والأزرار الخاصة بالتعديل من نموذج DOM، فإذا كانت قيمة المتغير editable هي false، فسيعيد إطار عمل Angular وضع العنصر <div> مع زرَّي "التعديل Edit" و"الحذف Delete" في نموذج DOM.

يؤدي النقر على زر "الحفظ Save" إلى استدعاء التابع saveItem()‎ الذي يأخذ القيمة من المتغير ‎#editedItem الخاص بالعنصر <input> ويغير وصف description العنصر إلى السلسلة النصية editedItem.value.

إعداد المكون AppComponent

سنضيف في القسم التالي شيفرةً برمجيةً تعتمد على الاتصال بالمكونين AppComponent و ItemComponent، إذ يجب إعداد المكون AppComponent أولًا عبر إضافة السطر التالي لاستيراد Item في بداية الملف app.component.ts:

import { Item } from "./item";

بعد ذلك اضبط المكون AppComponent بإضافة ما يلي في نفس ملف الصنف:

remove(item) {
  this.allItems.splice(this.allItems.indexOf(item), 1);
}

يستخدِم التابع remove()‎ تابع جافاسكربت Array.splice()‎ لإزالة عنصر واحد عند فهرس indexOf العنصر ذي الصلة، ويعني ذلك أن التابع splice()‎ يزيل العنصر من المصفوفة.

إضافة الشيفرة البرمجية أو المنطق إلى المكون ItemComponent

يمكن استخدام واجهة المستخدِم الخاصة بالمكوِّن ItemComponent من خلال إضافة شيفرة برمجية إلى المكون مثل إضافة دوال وطرق لدخول البيانات وخروجها.

عدّل تعليمات استيراد جافاسكربت في الملف item.component.ts كما يلي:

import { Component, Input, Output, EventEmitter } from '@angular/core';
import { Item } from "../item";

تسمح إضافة Input و Output و EventEmitter للمكوِّن ItemComponent بمشاركة البيانات مع المكوِّن AppComponent، كما يمكن أن يفهم المكوِّن ItemComponent ما هو العنصر item من خلال استيراد Item.

استبدل الصنف ItemComponent الناتج في الملف item.component.ts بما يلي:

export class ItemComponent {

  editable = false;

  @Input() item: Item;
  @Input() newItem: string;
  @Output() remove = new EventEmitter<Item>();

  saveItem(description) {
    if (!description) return;
    this.editable = false;
    this.item.description = description;
  }
}

تساعد الخاصية editable في تبديل قسم من القالب حيث يمكن للمستخدِم تعديل عنصر، إذ تُعَدّ الخاصية editable هي الخاصية نفسها في توصيف HTML ضمن تعليمة ‎*ngIf التي هي ‎*ngIf="editable"‎، فإذا أردتَ استخدام خاصية في القالب، فيجب التصريح عنها في الصنف.

يسهّل كل من ‎@Input()‎ و ‎@Output()‎ و EventEmitter الاتصال بين مكونيك، إذ يعمل ‎@Input()‎ بوصفه وسيلةً لدخول البيانات إلى المكوِّن، ويعمل ‎@Output()‎ بوصفه وسيلةً لخروج البيانات من المكوِّن، كما يجب أن يكون ‎@Output()‎ من النوع EventEmitter بحيث يمكن للمكوِّن رفع حدث عندما تكون هناك بيانات جاهزة لمشاركتها مع مكوِّن آخر.

استخدم ‎@Input()‎ لتحديد أن قيمة الخاصية يمكن أن تأتي من خارج المكوِّن، واستخدم ‎@Output()‎ مع EventEmitter لتحديد أن قيمة الخاصية يمكن أن تترك المكوِّن بحيث يمكن لمكوِّن آخر تلقي تلك البيانات.

يأخذ التابع saveItem()‎ وسيطًا هو الوصف description الذي يُعَدّ النص الذي يدخله المستخدِم في حقل الإدخال <input> عند تعديل عنصر في القائمة، كما يُعَدّ الوصف description السلسلة النصية نفسها التي تأتي من العنصر <input> مع متغير القالب ‎#editedItem.

إذا لم يدخِل المستخدِم أيّ قيمة ولكنه نقر على زر "الحفظ Save"، فلن يعيد التابع saveItem()‎ شيئًا ولن يُحدِّث الوصف description، فإذا لم تكن تعليمة if موجودةً، فيمكن للمستخدِم النقر على زر "الحفظ Save" بدون وجود أيّ شيء في حقل الإدخال <input> وسيصبح الوصف description سلسلةً نصيةً فارغةً.

إذا أدخل المستخدِم نصًا ونقر على زر "الحفظ Save"، فسيضبط التابع saveItem()‎ الخاصية editable على القيمة false، مما يتسبب في أن يزيل الموجّه ‎*ngIf في القالب ميزة التعديل ويصيّر أزرار "التعديل Edit" و"الحذف Delete" مرةً أخرى.

يجب أن يُصرَّف التطبيق في هذه المرحلة، ولكن يجب استخدام المكوِّن ItemComponent ضمن المكوِّن AppComponent لتتمكّن من رؤية الميزات الجديدة في المتصفح.

استخدام المكون ItemComponent ضمن المكون AppComponent

يمنحك تضمين أحد المكونات ضمن مكوِّن آخر في سياق علاقة المكوِّن الأب بالمكوِّن الابن المرونةَ في استخدام المكونات حيثما تريد ذلك، إذ يُعَدّ المكوِّن AppComponent صدَفةً shell للتطبيق بحيث يمكنك تضمين مكونات أخرى.

يمكنك استخدام المكوِّن ItemComponent ضمن المكوِّن AppComponent من خلال وضع محدّد المكوِّن ItemComponent في قالب المكون AppComponent، كما يحدّد إطار عمل Angular محدّد المكوِّن في البيانات الوصفية لعنصر التصميم ‎@Component()‎، إذ يكون المحدّد Selector في مثالنا هو app-item:

@Component({
  selector: 'app-item',
  templateUrl: './item.component.html',
  styleUrls: ['./item.component.css']
})

يمكنك استخدام محدّد المكون ItemComponent ضمن المكوِّن AppComponent من خلال إضافة العنصر <app-item> الذي يتوافق مع المحدّد الذي عرّفته لصنف المكوِّن في الملف app.component.html، لذا استبدل القائمة غير المرتبة الحالية في الملف app.component.html بما يلي:

<h2>{{items.length}} <span *ngIf="items.length === 1; else elseBlock">item</span>
<ng-template #elseBlock>items</ng-template></h2>

<ul>
  <li *ngFor="let item of items">
    <app-item (remove)="remove(item)" [item]="item"></app-item>
  </li>
</ul>

تدخِل صيغة الأقواس المزدوجة المتعرجة {{}} في العنصر <h2> طول المصفوفة items وتعرضه.

يستخدِم العنصر <span> في عنصر العنوان <h2> الموجّه ‎*ngIf وelse لتحديد ما إذا كان يجب أن يشير العنصر <h2> إلى "العنصر item" أو مجموعة "العناصر items"، فإذا كان هناك عنصر واحد فقط في القائمة، فسيُعرَض العنصر <span> الذي يحتوي على الكلمة "عنصر item"، في حين إذا كان طول المصفوفة items هو أيّ شيء آخر غير القيمة 1، فسيظهر العنصر <ng-template> الذي أطلقنا عليه الاسم elseBlock بالصورة ‎#elseBlock بدلًا من العنصر <span>، كما يمكنك استخدام العنصر <ng-template> في إطار عمل Angular عندما لا تريد عرض المحتوى افتراضيًا، وبالتالي إذا كان طول مصفوفة العناصر items يساوي 1، فسيعرض الموجّه ‎*ngIf العنصر elseBlock وليس العنصر <span>.

يستخدِم العنصر <li> موجّه التكرار ‎*ngFor للمرور على جميع العناصر في مصفوفة items، إذ يُعَدّ الموجَّه ‎*ngFor الخاص بإطار عمل Angular موجّهًا آخرًا يساعدك على تغيير بنية نموذج DOM مع كتابة شيفرة برمجية أقل، كما يكرّر إطار عمل Angular العنصر <li> وكل شيء بداخله لكل عنصر item حيث يتضمن العنصر <li> العنصر <app-item>، وهذا يعني أنّ إطار عمل Angular ينشئ نسخةً أخرى من العنصر <app-item> لكل عنصر في المصفوفة، وبالتالي سينشئ إطار عمل Angular العديد من عناصر <li> لأيّ عدد من العناصر في المصفوفة، كما يمكنك استخدام الموجّه ‎*ngFor مع عناصر أخرى مثل <div> أو <span> أو <p>.

يحتوي المكوِّن AppComponent على التابع remove()‎ لإزالة العنصر المرتبط بالخاصية remove من المكوِّن ItemComponent، كما تربط الخاصية item الموجودة بين قوسين مربعين [] قيمة العنصر item بين المكونين AppComponent و ItemComponent.

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

إضافة التنسيق إلى المكون ItemComponent

يمكنك استخدام ملف تنسيق لمكوِّن لإضافة تنسيقات خاصة به، إذ يضيف تنسيق CSS التالي التنسيقات الأساسية ونموذج الصندوق المرن Flexbox للأزرار ومربعات الاختيار المخصَّصة.

الصق التنسيق التالي في الملف item.component.css:

.item {
  padding: .5rem 0 .75rem 0;
  text-align: left;
  font-size: 1.2rem;
}

.btn-wrapper {
  margin-top: 1rem;
  margin-bottom: .5rem;
}

.btn {
  /* menu buttons flexbox styles */
  flex-basis: 49%;
}

.btn-save {
  background-color: #000;
  color: #fff;
  border-color: #000;

}

.btn-save:hover {
  background-color: #444242;
}

.btn-save:focus {
  background-color: #fff;
  color: #000;
}

.checkbox-wrapper {
  margin: .5rem 0;
}

.btn-warn {
  background-color: #b90000;
  color: #fff;
  border-color: #9a0000;
}

.btn-warn:hover {
  background-color: #9a0000;
}

.btn-warn:active {
  background-color: #e30000;
  border-color: #000;
}

.sm-text-input {
  width: 100%;
  padding: .5rem;
  border: 2px solid #555;
  display: block;
  box-sizing: border-box;
  font-size: 1rem;
  margin: 1rem 0;
}

/* Custom checkboxes
Adapted from https://css-tricks.com/the-checkbox-hack/#custom-designed-radio-buttons-and-checkboxes */

/* Base for label styling */
[type="checkbox"]:not(:checked),
[type="checkbox"]:checked {
  position: absolute;
  left: -9999px;
}
[type="checkbox"]:not(:checked) + label,
[type="checkbox"]:checked + label {
  position: relative;
  padding-left: 1.95em;
  cursor: pointer;
}

/* checkbox aspect */
[type="checkbox"]:not(:checked) + label:before,
[type="checkbox"]:checked + label:before {
  content: '';
  position: absolute;
  left: 0; top: 0;
  width: 1.25em; height: 1.25em;
  border: 2px solid #ccc;
  background: #fff;
}

/* checked mark aspect */
[type="checkbox"]:not(:checked) + label:after,
[type="checkbox"]:checked + label:after {
  content: '\2713\0020';
  position: absolute;
  top: .15em; left: .22em;
  font-size: 1.3em;
  line-height: 0.8;
  color: #0d8dee;
  transition: all .2s;
  font-family: 'Lucida Sans Unicode', 'Arial Unicode MS', Arial;
}
/* checked mark aspect changes */
[type="checkbox"]:not(:checked) + label:after {
  opacity: 0;
  transform: scale(0);
}
[type="checkbox"]:checked + label:after {
  opacity: 1;
  transform: scale(1);
}

/* accessibility */
[type="checkbox"]:checked:focus + label:before,
[type="checkbox"]:not(:checked):focus + label:before {
  border: 2px dotted blue;
}

الخلاصة

يجب أن يكون لديك الآن تطبيق قائمة المهام باستخدام إطار عمل Angular الذي يمكنه إضافة العناصر وتعديلها وإزالتها، والخطوة التالية هي إضافة الترشيح Filtering بحيث يمكنك عرض العناصر التي تحقق معايير محددة.

ترجمة -وبتصرُّف- للمقال Creating an item component.

اقرأ أيضًا


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

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

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



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

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

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

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


×
×
  • أضف...