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

أحمد سلهب

الأعضاء
  • المساهمات

    8
  • تاريخ الانضمام

  • تاريخ آخر زيارة

آخر الزوار

لوحة آخر الزوار معطلة ولن تظهر للأعضاء

إنجازات أحمد سلهب

عضو مبتدئ

عضو مبتدئ (1/3)

1

السمعة بالموقع

  1. سنبني في هذا المقال لوحة كانبان kanban على الإنترنت باستخدام Angular ومنصة Firebase، حيث سيحتوي تطبيقنا النهائي على ثلاثة فئات من المهام وهي الأعمال المتأخرة والأعمال قيد الإنجاز والأعمال المنجزة، كما سنكون قادرين على إنشاء مهام وحذفها ونقلها من فئة إلى أخرى باستخدام عمليتي السحب والإفلات، وسنطوِّر واجهة المستخدِم باستخدام Angular وسنستخدِم قاعدة بيانات Firestore على أساس مخزن دائم لبيانات التطبيق، كما سننشر التطبيق في نهاية المقال على استضافة Firebase باستخدام واجهة سطر أوامر Angular CLI. ستتعلم في هذا المقال ما يلي: كيفية استخدام Angular material وعدة تطوير المكونات CDK. كيفية دمج Firebase مع تطبيق Angular الخاص بك. كيفية الحفاظ على بياناتك الدائمة في قاعدة بيانات Firestone. كيفية نشر تطبيقك إلى استضافة Firebase باستخدام Angular CLI وبأمر واحد فقط. يفترض هذا المقال بأنه لديك حساب جوجل Google وأساسيات Angular، كما ننصحك بالرجوع إلى المقالين مقدمة في مفاهيم Angular وما هي Angular؟ للتعرُّف على مفاهيم Angular الأساسية. 1. إنشاء مشروع جديد لننشئ مساحة عمل جديدة لإطار عمل Angular أولًا: ng new kanban-fire ? Would you like to add Angular routing? No ? Which stylesheet format would you like to use? CSS قد تستغرق هذه الخطوة بضع دقائق، حيث سينشئ Angular CLI بنية مشروعك ويُثبِّت جميع التبعيات المتعلقة به، كما عليك الانتقال إلى المجلد kanban-fire وتشغيل خادم تطوير عبر واجهة سطر الأوامر Angular CLI عندما تكتمل عملية التثبيت بتنفيذ الأمر التالي: ng serve افتح العنوان http://localhost:4200 لكي تشاهد صفحةً مشابهةً لهذه الصورة: افتح ملف src/app/app.component.html في المحرر الخاص بك واحذف كامل محتواه، إذ ستشاهد صفحة فارغة فقط عندما تعود مرةً ثانيةً إلى العنوان http://localhost:4200. 2. إضافة مكتبة Material وعدة CDK تتضمن Angular افتراضيًا مكونات واجهة المستخدِم المتوافقة مع مكتبة Material Design على أساس جزء من حزمة ‎@angular/material، حيث تكون إحدى التبعيات الخاصة بحزمة ‎@angular/material هي مجموعة تطوير المكونات Component Development Kit -أو CDK اختصارًا-، إذ تُوفِّر CDK الأوليات مثل أدوات a11y المساعدة والسحب والإفلات والتراكب، كما يمكننا الوصول إلى CDK عن طريق حزمة ‎@angular/cdk. نفِّذ الأمر التالي لإضافة مكتبة Material Design إلى التطبيق الخاص بك: ng add @angular/material يطلب منك الأمر السابق اختيار سمة لتطبيقك في حال كنت ترغب في استخدام تنسيقات الطباعة للمواد العالمية وإعداد تحريكات المتصفح للمواد Angular، وبعدها اختر "Indigo/Pink" للحصول على النتيجة نفسها الموجودة ضمن المقال، وأجب "نعم Yes" من أجل السؤالين الأخيرين، كما يثبِّت الأمر ng add الحزمة ‎@angular/material وتبعياته، ويستورد BrowserAnimationsModule في AppModule، ويمكننا استخدام المكونات التي تقدمها هذه الوحدة في الخطوة التالية. أضف أولًا شريط أدوات وأيقونة إلى AppComponent من خلال فتح ملف app.component.html وإضافة الشيفرة التالية: الملف src/app/app.component.html: <mat-toolbar color="primary"> <mat-icon>local_fire_department</mat-icon> <span>Kanban Fire</span> </mat-toolbar> أضفنا هنا شريط أدوات باستخدام اللون الأساسي لسمة Material Design واستخدمنا داخله أيقونة local_fire_depeartment بجانب العنوان Kanban Fire، فإذا نظرت إلى الطرفية الخاصة بك، فسترى أنَّ Angular يرمي لك بعض الأخطاء، ومن أجل إصلاحها تأكَّد من إضافة الاستيرادات التالية ضمن ملف AppModule: الملف src/app/app.module.ts: ... import { MatToolbarModule } from '@angular/material/toolbar'; import { MatIconModule } from '@angular/material/icon'; @NgModule({ declarations: [ AppComponent ], imports: [ ... MatToolbarModule, MatIconModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } نحتاج إلى استيراد الوحدات المقابلة لها في ملف AppModule نظرًا لأننا استخدمنا شريط أدوات وأيقونة عن طريق Angular material، فيجب عليك رؤية النتيجة التالية على الشاشة: ليست نتيجةً سيئةً بما أنك لم تكتب سوى أربعة سطور من HTML واستوردت مرتين فقط. 3. عرض المهام سننشئ الآن مكونًا نستطيع استخدامه لتمثيل المهام وعرضها في لوحة kanban، لذا انتقل إلى مجلد src/app ونفِّذ أمر CLI التالي: ng generate component task ينشئ هذا الأمر TaskComponent ويضيف التعريف الخاص به إلى ملف AppModule، وبعدها أنشئ ملف يسمى task.ts ضمن دليل task، حيث سنستخدم هذا الملف لتعريف واجهة المهام في لوحة kanban، حيث سيكون لكل مهمة ثلاثة حقول اختيارية هي id وtitle وdescription وجميعها من نوع سلسلة نصية: الملف src/app/task/task.ts: export interface Task { id?: string; title: string; description: string; } لنحدِّث الآن ملف task.component.ts لكي يقبل مكون TaskComponent إدخالات من نوع كائن من النوع task ولكي يكون قادرًا على إصدار مخرجات "edit": الملف src/app/task/task.component.ts: import { Component, Input, Output, EventEmitter } from '@angular/core'; import { Task } from './task'; @Component({ selector: 'app-task', templateUrl: './task.component.html', styleUrls: ['./task.component.css'] }) export class TaskComponent { @Input() task: Task | null = null; @Output() edit = new EventEmitter<Task>(); } عدِّل قالب TaskComponent عن طريق استبدال شيفرة HTML التالية بمحتوى الملف task.component.html: الملف src/app/task/task.component.html: <mat-card class="item" *ngIf="task" (dblclick)="edit.emit(task)"> <h2>{{ task.title }}</h2> <p> {{ task.description }} </p> </mat-card> لاحظ أنه توجد لدينا أخطاء تظهر في الطرفية: 'mat-card' is not a known element: 1. If 'mat-card' is an Angular component, then verify that it is part of this module. 2. If 'mat-card' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message.ng نستخدِم مكون mat-card في القالب السابق الموجود في حزمة ‎@angular/material، ولكنا لم تستورد الوحدة الخاصة به في التطبيق، ولإصلاح هذا الخطأ نحتاج إلى استيراد الوحدة MatCardModule في ملف AppModule: الملف src/app/app.module.ts: ... import { MatCardModule } from '@angular/material/card'; @NgModule({ declarations: [ AppComponent ], imports: [ ... MatCardModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } ستنشئ الآن بعض المهام في AppComponent ونتصوَّرها باستخدام TaskComponent، لذا عرِّف مصفوفةً باسم todo في AppComponent وأضف مهمتين داخلها: الملف src/app/app.component.ts: ... import { Task } from './task/task'; @Component(...) export class AppComponent { todo: Task[] = [ { title: 'Buy milk', description: 'Go to the store and buy milk' }, { title: 'Create a Kanban app', description: 'Using Firebase and Angular create a Kanban app!' } ]; } أضف موجِّه ‎*ngFor في نهاية ملف app.component.html كما يلي: الملف src/app/app.component.html: <app-task *ngFor="let task of todo" [task]="task"></app-task> ستشاهد ما يلي عندما تفتح نافذة المتصفح: 4. تضمين السحب والإفلات للمهام نحن جاهزون الآن لتنفيذ الجزء الممتع من التطبيق، لذا دعنا ننشئ ثلاث حاويات من أجل الحالات الثلاث المختلفة للمهام وضمّن وظيفة السحب والإفلات في التطبيق باستخدام Angular CDK. احذف المكوِّن app-task مع الموجِّه ‎*ngFor الخاص به في أعلى الملف app.component.html واستبدل الشيفرة التالية به: الملفsrc/app/app.component.html: <div class="container-wrapper"> <div class="container"> <h2>Backlog</h2> <mat-card cdkDropList id="todo" #todoList="cdkDropList" [cdkDropListData]="todo" [cdkDropListConnectedTo]="[doneList, inProgressList]" (cdkDropListDropped)="drop($event)" class="list"> <p class="empty-label" *ngIf="todo.length === 0">Empty list</p> <app-task (edit)="editTask('todo', $event)" *ngFor="let task of todo" cdkDrag [task]="task"></app-task> </mat-card> </div> <div class="container"> <h2>In progress</h2> <mat-card cdkDropList id="inProgress" #inProgressList="cdkDropList" [cdkDropListData]="inProgress" [cdkDropListConnectedTo]="[todoList, doneList]" (cdkDropListDropped)="drop($event)" class="list"> <p class="empty-label" *ngIf="inProgress.length === 0">Empty list</p> <app-task (edit)="editTask('inProgress', $event)" *ngFor="let task of inProgress" cdkDrag [task]="task"></app-task> </mat-card> </div> <div class="container"> <h2>Done</h2> <mat-card cdkDropList id="done" #doneList="cdkDropList" [cdkDropListData]="done" [cdkDropListConnectedTo]="[todoList, inProgressList]" (cdkDropListDropped)="drop($event)" class="list"> <p class="empty-label" *ngIf="done.length === 0">Empty list</p> <app-task (edit)="editTask('done', $event)" *ngFor="let task of done" cdkDrag [task]="task"></app-task> </mat-card> </div> </div> يبدو أنَّ هناك الكثير من الأمور التي تحدث في الشيفرة السابقة، لذلك سنتناول كل قسم منها على حدة، حيث تُعَدّ الشيفرة التالية بنية المستوى الأعلى للقالب: الملف src/app/app.component.html: ... <div class="container-wrapper"> <div class="container"> <h2>Backlog</h2> ... </div> <div class="container"> <h2>In progress</h2> ... </div> <div class="container"> <h2>Done</h2> ... </div> </div> أنشأنا هنا وسم div يحيط بجميع الحاويات الثلاث مع صنف باسم "container-wrapper" ويوجد لكل حاوية صنف باسم "container" كما يوجد عنوان لها ضمن وسم h2، فانظر الآن إلى بنية أول حاوية: الملف src/app/app.component.html: ... <div class="container"> <h2>Backlog</h2> <mat-card cdkDropList id="todo" #todoList="cdkDropList" [cdkDropListData]="todo" [cdkDropListConnectedTo]="[doneList, inProgressList]" (cdkDropListDropped)="drop($event)" class="list" > <p class="empty-label" *ngIf="todo.length === 0">Empty list</p> <app-task (edit)="editTask('todo', $event)" *ngFor="let task of todo" cdkDrag [task]="task"></app-task> </mat-card> </div> ... عرَّفنا أولًا الحاوية على أنها mat-card والتي تستخدِم موجِّه cdkDropList، كما استخدمنا هنا mat-card بسبب التنسيقات التي يوفرها هذا المكوِّن، وسيسمح لنا cdkDropList لاحقًا بإفلات المهام داخل العنصر، كما عيَّنا اثنين من المدخلات كما يلي: cdkDropListData: تمثِّل إدخالًا للقائمة المنسدلة التي تسمح لنا بتحديد مصفوفة البيانات. cdkDropListConnectedTo: هو مرجع إلى cdkDropLists الأخرى والتي يتصل بها cdkDropList الحالي، حيث سنحدد أي القوائم الأخرى التي يمكننا إسقاط العناصر ضمنها من خلال تعيين هذا الإدخال. نريد بالإضافة إلى ذلك التعامل مع حدث الإفلات عن طريق استخدام خرج cdkDropListDropped، حيث سنستدعي تابع drop المعرَّف ضمن AppComponent كما نمرر له الحدث الحالي على أساس وسيط بمجرد أن يُصدر cdkDropList الخرج، ولاحظ أنه استخدمنا id أيضًا على أساس معرِّف لهذه الحاوية واسم class حتى تتمكن من تنسيقه، كما سننلق نظرةً على محتوى أبناء mat-card وسنجد العنصرَين التاليين ضمنها وهما: فقرة تستخدمها لإظهار نص بأنَّ "القائمة فارغة" عندما لا توجد أية عناصر في قائمة todo. مكوِّن app-task، ولاحظ هنا بأننا تتعامل مع خرج edit الذي عرّفناه بالأصل عن طريق استدعاء تابع editTask وتمرير اسم القائمة وكائن ‎$event لها، حيث سيساعدنا ذلك في استبدال المهمة المحرَّرة من القائمة الصحيحة، ثم نكرِّر الخطوات السابقة على قائمة todo ونمرِّر دخل task له، كما نضيف في هذه المرة أيضًا موجِّه cdkDrag الذي يجعل المهام الفردية قابلةً للسحب. نحتاج إلى تحديث ملف app.module.ts وتضمين استيراد وحدة DragDropModule لكي تعمل الشيفرة السابقة: الملف src/app/app.module.ts: ... import { DragDropModule } from '@angular/cdk/drag-drop'; @NgModule({ declarations: [ AppComponent ], imports: [ ... DragDropModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } نحتاج أيضًا إلى تعريف مصفوفات inProgress وdone في الملف نفسه الذي عرَّفت فيه كل من توابع editTask وdrop. الملف src/app/app.component.ts: ... import { CdkDragDrop, transferArrayItem } from '@angular/cdk/drag-drop'; @Component(...) export class AppComponent { todo: Task[] = [...]; inProgress: Task[] = []; done: Task[] = []; editTask(list: string, task: Task): void {} drop(event: CdkDragDrop<Task[]|null>): void { if (event.previousContainer === event.container) { return; } if (!event.container.data || !event.previousContainer.data) { return; } transferArrayItem( event.previousContainer.data, event.container.data, event.previousIndex, event.currentIndex ); } } لاحظ أنه تحققنا أولًا في تابع drop من حالة إفلات المهمة في القائمة نفسها التي كانت موجودةً فيها أصلًا وفي هذه الحالة لا نفعل أي شيء؛ أما في حال كان الإفلات في قائمة جديدة، فسننقل المهمة الحالية إلى الحاوية الوجهة. يجب أن تكون النتيجة كما يلي: ستكون قادرًا على نقل العناصر بين قائمتين عندما تصل لهذه النقطة. 5. إنشاء مهام جديدة يجب علينا تحديث قالب AppComponent كما يلي لتضمين الوظيفة المسؤولة عن إنشاء مهام جديدة: الملف src/app/app.component.html: <mat-toolbar color="primary"> ... </mat-toolbar> <div class="content-wrapper"> <button (click)="newTask()" mat-button> <mat-icon>add</mat-icon> Add Task </button> <div class="container-wrapper"> <div class="container"> ... </div> </div> ننشئ عنصر div في المستوى الأعلى حول container-wrapper وتضيف أيضًا زر بأيقونة "add" من نوع Material بجانب عنوان "إضافة مهمة Add Task"، حيث نحتاج إلى مغلِّف إضافي لوضع الزر في أعلى القائمة الخاصة بكل حاوية والتي سنضعها لاحقًا بجانب بعضها بعضًا باستخدام flexbox، كما نحتاج إلى استيراد الوحدة المرتبطة به ضمن ملف AppModule لأن هذا الزر يستخدم مكوِّن الزر من نوع Material: الملف src/app/app.module.ts: ... import { MatButtonModule } from '@angular/material/button'; @NgModule({ declarations: [ AppComponent ], imports: [ ... MatButtonModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } سنضمِّن الوظيفة المسؤولة عن إضافة المهام في AppComponent عن طريق استخدام مربع حوار من نوع Material، حيث سيكون لدينا ضمن مربع الحوار استمارةً بحقلين هما العنوان والوصف، وبالتالي عندما ينقر المستخدِم على زر "إضافة مهمة Add Task"، فسنفتح له مربع الحوار، وعندما يرسل المستخدِم هذه الاستمارة، فسنضف مهمةً جديدةً إلى قائمة todo، كما سنلقِ نظرةً على التضمين عالي المستوى لهذه الوظيفة في AppComponent: ... import { MatDialog } from '@angular/material/dialog'; @Component(...) export class AppComponent { ... constructor(private dialog: MatDialog) {} newTask(): void { const dialogRef = this.dialog.open(TaskDialogComponent, { width: '270px', data: { task: {}, }, }); dialogRef .afterClosed() .subscribe((result: TaskDialogResult) => this.todo.push(result.task)); } } عرَّفنا بانيًا للمكوِّن في الشفرة السابقة وحقنّا صنف MatDialog ضمنه، وسنفعل ضمن دالة newTask ما يلي: نفتح مربع حوار جديد باستخدام TaskDialogComponent الذي سنعرِّفه لاحقًا. نحدِّد عرض مربع الحوار ليكون 270px. نمرِّر مهمةً فارغةً إلى مربع الحوار على أساس بيانات، حيث سنكون قادرين على الحصول على مرجع إلى الكائن الحاوي لهذه البيانات في TaskDialogComponent. نشترك بحدث الإغلاق close ونضيف المهمة من الكائن result إلى المصفوفة todo. يجب أولًا استيراد وحدة MatDialogModule في ملف AppModule لتتأكد من أنّ كل شيء يعمل بصورة صحيحة : الملف src/app/app.module.ts: ... import { MatDialogModule } from '@angular/material/dialog'; @NgModule({ declarations: [ AppComponent ], imports: [ ... MatDialogModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } لننشئ الآن مكوِّن TaskDialogComponentعن طريق الانتقال إلى الدليل src/app وتنفيذ الأمر التالي: ng generate component task-dialog افتح ملف الآتي: src/app/task-dialog/task-dialog.component.html من أجل تضمين الوظائف الخاصة به واستبدال ما يلي بمحتواه: الملف src/app/task-dialog/task-dialog.component.html: <mat-form-field> <mat-label>Title</mat-label> <input matInput cdkFocusInitial [(ngModel)]="data.task.title" /> </mat-form-field> <mat-form-field> <mat-label>Description</mat-label> <textarea matInput [(ngModel)]="data.task.description"></textarea> </mat-form-field> <div mat-dialog-actions> <button mat-button [mat-dialog-close]="{ task: data.task }">OK</button> <button mat-button (click)="cancel()">Cancel</button> </div> ننشئ في القالب أعلاه استمارةً بحقلين هما title وdescription، كما نستخدِم الموجِّه cdkFocusInput لجعل التركيز على حقل الإدخال title تلقائيًا عندما يفتح المستخدِم مربع الحوار، ولاحظ كيف نستخدِم خاصية data للمكوِّن على أساس مرجع داخل القالب، حيث ستشبه هذه تمامًا data التي نمررها للتابع open في AppComponent. كما نستخدِم تربيط البيانات ثنائي الاتجاه باستخدام ngModel من أجل تحديث العنوان ووصف المهمة عندما يغيِّر المستخدِم محتوى هذه الحقول، وعندما ينقر المستخدِم على زر موافق، فسنُعيد النتيجة { task: data.task } تلقائيًا وهي المهمة التي بدّلناها باستخدام حقول الاستمارة في القالب أعلاه، كما سنُضمِّن الآن المتحكم الخاص بالمكوِّن كما يلي: الملف src/app/task-dialog/task-dialog.component.ts: import { Component, Inject } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Task } from '../task/task'; @Component({ selector: 'app-task-dialog', templateUrl: './task-dialog.component.html', styleUrls: ['./task-dialog.component.css'], }) export class TaskDialogComponent { private backupTask: Partial<Task> = { ...this.data.task }; constructor( public dialogRef: MatDialogRef<TaskDialogComponent>, @Inject(MAT_DIALOG_DATA) public data: TaskDialogData ) {} cancel(): void { this.data.task.title = this.backupTask.title; this.data.task.description = this.backupTask.description; this.dialogRef.close(this.data); } } نحقن في الشيفرة السابقة مرجعًا في مربع الحوار ضمن TaskDialogComponent حتى نتمكن من إغلاقه، كما نحقن أيضًا القيمة الخاصة بالمزود المرتبطة مع مفتاح MAT_DIALOG_DATA والذي يمثل كائن البيانات الذي مرَّرناه إلى التابع المفتوح في AppComponent، ونعرِّف أيضًا الخاصية الخاصة backupTask والتي هي نسخة من المهمة التي مررناها مع كائن البيانات. سنستعيد خصائص this.data.task التي ربما تكون قد تغيّرت ونغلق مربع الحوار ونمرِّر this.data على أساس نتيجة عندما يضغط المستخدِم على زر "إلغاء"، وهناك نوعان من المراجع التي استخدمناها ولكن لم تُعرَّف بعد وهما: TaskDialogData TaskDialogResult وأضف الآن التعريفات التالية إلى أسفل الملف: الملف src/app/task-dialog/task-dialog.component.ts: ... export interface TaskDialogData { task: Partial<Task>; enableDelete: boolean; } export interface TaskDialogResult { task: Task; delete?: boolean; } الشيء الأخير الذي نحتاج إلى فعله قبل جهوزية الوظيفة هو استيراد بعض الوحدات في ملف AppModule. الملف src/app/app.module.ts: ... import { MatInputModule } from '@angular/material/input'; import { FormsModule } from '@angular/forms'; @NgModule({ declarations: [ AppComponent ], imports: [ ... MatInputModule, FormsModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } ستشاهد الآن واجهة المستخدِم التالية عندما تنقر على زر "إضافة مهمة Add Task": 6. تحسين تنسيقات التطبيق سنحسِّن التخطيط الخاص بالتطبيق بحيث يتحسّن مظهر التطبيق وذلك عن طريق تغيير وتبديل تنسيقاته بصورة بسيطة، حيث ستضع الحاويات بجانب بعضها البعض كما سنصيف بعض التعديلات الطفيفة على زر "إضافة مهمة Add Task" وعنوان القائمة فارغة، لذا افتح ملف src/app/app.component.css وأضف التنسيقات التالية إلى أسفله: الملف src/app/app.component.css: mat-toolbar { margin-bottom: 20px; } mat-toolbar > span { margin-left: 10px; } .content-wrapper { max-width: 1400px; margin: auto; } .container-wrapper { display: flex; justify-content: space-around; } .container { width: 400px; margin: 0 25px 25px 0; } .list { border: solid 1px #ccc; min-height: 60px; border-radius: 4px; } app-new-task { margin-bottom: 30px; } .empty-label { font-size: 2em; padding-top: 10px; text-align: center; opacity: 0.2; } ضبطنا في الشيفرة أعلاه التخطيط الخاص بشريط الأدوات وعنوانه، كما تأكدنا من محاذاة المحتوى أفقيًا عن طريق تعيين عرضه إلى 1400px وهامشه إلى auto، ثم وضعنا الحاويات بجانب بعضها بعضًا باستخدام flexbox، وأخيرًا أجرينا بعض التعديلات في كيفية تصوّر المهام والقوائم الفارغة، ويجب عليك مشاهدة واجهة المستخدِم التالية عند إعادة تحميل تطبيقك: لا يزال لدينا مشكلةً مزعجةً تحدث عندما ننقل المهام بين الحاويات على الرغم من تحسين تنسيقات تطبيقنا بصورة كبيرة: نرى بطاقتين للمهمة نفسها -أي واحدة للمهمة التي نسحبها وأخرى للمهمة الموجودة في الحاوية نفسها- عندما نبدأ في سحب مهمة "شراء الحليب Buy milk"، في حين توفِّر لنا عدة التطوير CDK في Angular أسماء أصناف CSS التي تمكِّننا من إصلاح هذه المشكلة، والآن أضف التنسيق التالي في نهاية ملف src/app/app.component.css: الملف src/app/app.component.css: .cdk-drag-animating { transition: transform 250ms; } .cdk-drag-placeholder { opacity: 0; } بينما نسحب عنصر ما، سيستنسخه سحب وإفلات Angular CDK ويدرجه في الموضع الذي سنُفلِت الأصل فيه، كما عيَّنا خاصية الشفافية في صنف cdk-drag-placeholder للتأكد من عدم ظهور هذا العنصر عند عملية النقل والتي سيضيفها CDK إلى العنصر النائب. بالإضافة إلى ذلك فعندما تفلت عنصرًا فسيضيف CDK صنف cdk-drag-animating، ولإضافة تحريك سلس للعنصر بدلًا من نقل العنصر مباشرةً فإننا نعرِّف انتقالًا بمدة 250ms؛ أما لإجراء بعض التعديلات الطفيفة على التنسيقات الخاصة بالمهام، فسنذهب إلى ملف task.component.css ونعيّن طريقة عرض العنصر إلى block ونضبط بعض الهوامش كما يلي: الملف src/app/task/task.component.css: :host { display: block; } .item { margin-bottom: 10px; cursor: pointer; } 7. تحرير وحذف المهام الحالية سنعيد استخدام معظم الوظائف التي ضمّنَّاها مسبقًا لتحرير المهام الحالية وإزالتها، فعندما ينقر المستخدِم نقرًا مزدوجًا على مهمة ما، فسنفتح TaskDialogComponent ونملأ الحقلين الموجودين في الاستمارة بقيمتي title وdescription الخاصَين بالمهمة. كما سنضيف في مكون TaskDialogComponent أيضًا زر الحذف، فعندما ينقر المستخدِم عليه سنمرِّر تعليمة الحذف والتي ستنتهي في ملف AppComponent، ويكون التغيير الوحيد الذي نحتاج إلى فعله في ملف TaskDialogComponent هو في القالب الخاص به: الملف src/app/task-dialog/task-dialog.component.html: <mat-form-field> ... </mat-form-field> <div mat-dialog-actions> ... <button *ngIf="data.enableDelete" mat-fab color="primary" aria-label="Delete" [mat-dialog-close]="{ task: data.task, delete: true }"> <mat-icon>delete</mat-icon> </button> </div> يعرض هذا الزر أيقونة الحذف المنسقة بتنسيق مكتبة Material الافتراضي، فعندما ينقر المستخدِم عليه، فسنغلق مربع الحوار ونمرِّر الكائن الآتي: { task: data.task, delete: true } على أساس نتيجة لذلك، ولاحظ أيضًا أننا جعلنا الزر دائريًا عن طريق استخدام mat-fab، كما جعلنا لونه مثل اللون الأساسي المستخدَم في التطبيق، وحدَّدنا ظهوره فقط عندما يمكن حذف بيانات مربع الحوار؛ أما القسم المتبقي من عملية التضمين لكل من وظيفتي التحرير والحذف للمهام فهو موجود في ملف AppComponent بحيث كل ما يجب عليك القيام به هو استبدل تابع editTask بما يلي: الملف src/app/app.component.ts: @Component({ ... }) export class AppComponent { ... editTask(list: 'done' | 'todo' | 'inProgress', task: Task): void { const dialogRef = this.dialog.open(TaskDialogComponent, { width: '270px', data: { task, enableDelete: true, }, }); dialogRef.afterClosed().subscribe((result: TaskDialogResult) => { const dataList = this[list]; const taskIndex = dataList.indexOf(task); if (result.delete) { dataList.splice(taskIndex, 1); } else { dataList[taskIndex] = task; } }); } ... } إذا نظرت إلى وسائط تابع editTask فستجد: قائمةً تحوي أحد الأنواع 'done' | 'todo' | 'inProgress' والتي تمثِّل نوعًا موحَّدًا من سلسلة نصية مع قيم مقابلة للخصائص المرتبطة بكل حاوية. المهمة الحالية التي تريد تحريرها. أنشئ في جسم التابع نسخةً من TaskDialogComponent لأن data الخاصة به تُمرَّر على أساس كائن وهو الذي يحدِّد المهمة التي نريد تحريرها ويمكِّن زر التحرير في الاستمارة أيضًا عن طريق إسناد الخاصية enableDelete إلى القيمة true، وعندما نحصل على النتيجة المُعادة من مربع الحوار فسنتعامل مع حالتين: عند إسناد راية delete إلى true -أي عندما يضغط المستخدِم على زر الحذف- فسنزيل المهمة من القائمة الموجودة ضمنها. وإلا فسنستبدل فقط المهمة ذات الدليل المعطى بالمهمة التي حصلنا عليها من نتيجة مربع الحوار. 8. إنشاء مشروع Firebase وربطه بالمشروع لتنشأ مشروع Firebase جديد، انتقل إلى Firebase Console. أنشئ مشروعًا جديدًا باسم "KanbanFire". سنربط الآن مشروعنا مع Firebase، حيث يقدِّم فريق Firebase حزمة ‎@angular/fire والتي توفر عملية التكامل بين التقنيتين، ولإضافة دعم Firebase إلى تطبيقك افتح الدليل الجذر لمساحة العمل الخاصة بتطبيقك ونفِّذ الأمر التالي: ng add @angular/fire يثبِّت هذا الأمر حزمة ‎@angular/fire ويسألك بعض الأسئلة، ويجب عليك رؤية مثل هذه الصورة في الطرفية الخاصة بك: تفتح عملية التثبيت في غضون ذلك نافذة المتصفح حتى تتمكن من المصادقة باستخدام حساب Firebase الخاص بك، ويطلب منك أخيرًا اختيار مشروع Firebase وينشئ بعض الملفات على القرص الخاص بك، وبعد ذلك نحتاج إلى إنشاء قاعدة بيانات Firestore، إذ يمكنك فعل ذلك من خلال التوجه إلى "Cloud Firestore" ثم النقر فوق "Create Database". أنشئ بعد ذلك قاعدة بيانات في وضع الاختبار: أخيرًا، حدِّد المنطقة: الشيء الأخير الذي يجب عليك فعله هو إضافة تهيئة Firebase إلى بيئتك، حيث يمكنك العثور على تهيئة المشروع الخاص بك في Firebase Console. انقر على أيقونة الإعدادات بجوار نظرة عامة عن المشروع Project Overview. اختر إعدادات المشروع Project Settings. حدِّد "تطبيق ويب Web app" ضمن "تطبيقاتك Your apps": سجّل تطبيقك بعد ذلك وتأكد من نفعيل "Firebase Hosting": يمكنك نسخ التهيئة الخاصة بك إلى ملف src/environments/environment.ts بعد أن تنقر على "تسجيل التطبيق Register app": في النهاية، يجب أن يبدو ملف التكوين الخاص بك كما يلي: export const environment = { production: false, firebase: { apiKey: '<your-key>', authDomain: '<your-project-authdomain>', databaseURL: '<your-database-URL>', projectId: '<your-project-id>', storageBucket: '<your-storage-bucket>', messagingSenderId: '<your-messaging-sender-id>' } }; 9. نقل البيانات إلى Firestore استخدم الآن ‎@angular/fire لنقل بياناتك إلى Firestore بعد أن أعددت Firebase SDK، لذا سنستورد أولًا الوحدات التي سنحتاجها في ملف AppModule: ... import { environment } from 'src/environments/environment'; import { AngularFireModule } from '@angular/fire'; import { AngularFirestoreModule } from '@angular/fire/firestore'; @NgModule({ declarations: [AppComponent, TaskDialogComponent, TaskComponent], imports: [ ... AngularFireModule.initializeApp(environment.firebase), AngularFirestoreModule ], providers: [], bootstrap: [AppComponent], }) export class AppModule {} يجب علينا حقن AngularFirestore في باني AppComponent بما أن التطبيق الخاص بنا يستخدِم Firestone: الملف src/app/app.component.ts: ... import { AngularFirestore } from '@angular/fire/firestore'; @Component({...}) export class AppComponent { ... constructor(private dialog: MatDialog, private store: AngularFirestore) {} ... } نحدِّث بعد ذلك الطريقة التي نهيئ بها مصفوفات الحاويات: الملف src/app/app.component.ts: ... @Component({...}) export class AppComponent { todo = this.store.collection('todo').valueChanges({ idField: 'id' }) as Observable<Task[]>; inProgress = this.store.collection('inProgress').valueChanges({ idField: 'id' }) as Observable<Task[]>; done = this.store.collection('done').valueChanges({ idField: 'id' }) as Observable<Task[]>; ... } استخدمنا هنا AngularFirestore للحصول على محتوى التجميعة من قاعدة البيانات مباشرةً، ولاحظ أنّ valueChanges يُعيد observables عوضًا عن مصفوفة، ولاحظ أيضًا أننا حدَّدنا أن حقل id للمستندات في هذه التجميعة يجب أن يُسمى أيضًا id ليطابق الاسم الذي استخدمناه في الواجهة Task؛ أما observables التي أُعيدت عن طريق valueChanges فستُصدر تجميعةً من المهام في أي وقت تتغير فيه. نظرًا لأننا نتعامل مع المرصودات observables بدلًا من المصفوفات فأنت بحاجة إلى تحديث طريقة إضافة المهام وإزالتها وتعديلها، وأيضًا تحديث الوظيفة المسؤولة عن نقل المهام بين الحاويات وعوضًا عن تغيير المصفوفات في الذاكرة ستستخدم Firebase SDK لتحديث البيانات في قاعدة البيانات، وسنرى كيف ستبدو الشيفرة بعد إعادة ترتيبها، لذا استبدل التابع drop الموجود في ملف src/app/app.component.ts بدايةً: الملف src/app/app.component.ts: drop(event: CdkDragDrop<Task[]>): void { if (event.previousContainer === event.container) { return; } const item = event.previousContainer.data[event.previousIndex]; this.store.firestore.runTransaction(() => { const promise = Promise.all([ this.store.collection(event.previousContainer.id).doc(item.id).delete(), this.store.collection(event.container.id).add(item), ]); return promise; }); transferArrayItem( event.previousContainer.data, event.container.data, event.previousIndex, event.currentIndex ); } سنزيل المهمة من التجميعة الأولى ونضيفها إلى التجميعة الثانية وذلك من أجل نقل مهمة من الحاوية الحالية إلى الحاوية الهدف، وبما أننا نؤدي هاتين العمليتين ونريد أن تظهرا على أساس عملية واحدة -أي جعل العملية ذَرية-، فسننفِّذها عن طريق معاملة Firestore، كما سنحدِّث تابع editTask حتى نتمكن من استخدام Firestore، لذا سنحتاج إلى تغيير الأسطر التالية من الشيفرة بداخل المعالج الخاص بمربع الإغلاق:: الملف src/app/app.component.ts: ... dialogRef.afterClosed().subscribe((result: TaskDialogResult) => { if (result.delete) { this.store.collection(list).doc(task.id).delete(); } else { this.store.collection(list).doc(task.id).update(task); } }); ... نستطيع الوصول إلى المستند المستهدَف والمقابل للمهمة التي نريد التعديل عليها باستخدام Firestore SDK ويمكنك حذفه أو تحديثه، ونحتاج أخيرًا إلى تحديث التابع المسؤول عن إنشاء مهام جديدة من خلال استبدال السطر this.todo.push('task')‎ بالسطر this.store.collection('todo').add(result.task)‎. لاحظ أن التجميعات الآن ليست مصفوفات وإنما مرصودات observables، ونحتاج إلى تحديث قالب AppComponent فقط لكي نتمكَّن من تصوّرهم عن طريق استبدال كل وصول إلى خصائص todo وinProgress وdone بالخصائص التالية todo | async وinProgress | async وdone | async على التتالي، كما يشترك أنبوب async تلقائيًا في المرصودات observables المرتبطة بالتجميعات، وتُشغِّل Angular تلقائيًا الكشف عن حدوث التغييرات ومعالجة المصفوفة المصدَرة عندما تُصدِر المرصودات قيمةً جديدةً، وفيما يلي التغييرات التي ستجريها في حاوية todo على سبيل المثال: الملف src/app/app.component.html: <mat-card cdkDropList id="todo" #todoList="cdkDropList" [cdkDropListData]="todo | async" [cdkDropListConnectedTo]="[doneList, inProgressList]" (cdkDropListDropped)="drop($event)" class="list"> <p class="empty-label" *ngIf="(todo | async)?.length === 0">Empty list</p> <app-task (edit)="editTask('todo', $event)" *ngFor="let task of todo | async" cdkDrag [task]="task"></app-task> </mat-card> نطبِّق أنبوب async عندما نمرِّر البيانات إلى الموجّه cdkDropList، حيث تفعل الشيء ذاته أيضًا داخل الموجِّه ‎*ngIf ولكن لاحظ أننا نستخدِم أيضًا تسلسلًا اختياريًا -والذي يُعرّف أيضًا باسم معامِل التنقل الآمن في Angular- عندما نحاول الوصول إلى خاصية length لضمان عدم حصولنا على خطأ وقت التشغيل runtime error في حال لم تكن قيمة todo | async إما null أو undefined، والآن يجب أن تشاهد شيئًا يشبه الصورة التالية عندما تنشئ مهمةً جديدةً في واجهة المستخدِم وتفتح Firestore: 10. تحسين عمليات التحديث بشكل أفضل سنؤدي حاليًا عمليات التحديث في التطبيق الخاص بك بصورة أفضل، حيث يوجد لدينا مصدر خدمة في Firestore، ولكن في الوقت نفسه لدينا نسخًا محليةً من المهام، وبالتالي عندما تُصدر أي مرصودات مرتبطة بالتجميعات أيَّ تغيير، فسنحصل على مصفوفة من المهام، وعندما يُغير إجراء المستخدِم من الحالة، فسنحدِّث القيم المحلية أولًا ثم نرسِل عملية التغيير إلى Firestore؛ أما عندما ننقل مهمةً من حاوية إلى أخرى، فسنستدعي transferArrayItem التي تعمل على النسخ المحلية من المصفوفات والتي تمثِّل المهام الموجودة في كل حاوية. يعامل Firebase SDK هذه المصفوفات على أنها غير قابلة للتغيير، مما يعني أنه سنحصل على نسخ جديدة منها أيضًا والتي ستعيد الحالة السابقة قبل نقل المهمة في المرة القادمة التي تُشغّل فيها Angular الكاشف عن حدوث تغييرات، والوقت نفسه نشغِّل التحديث في Firestore ويشغِّل Firebase SDK عملية التحديث بالقيم الصحيحة، وبالتالي ستُعرَض واجهة المستخدِم بالصورة الصحيحة، وبهذه الطريقة تُنقَل المهمة من القائمة الأولى إلى القائمة الثانية كما توضحه الصورة التالية: تختلف الطريقة الصحيحة لحل هذه المشكلة من تطبيق لآخر، ولكن نحتاج في جميع الحالات إلى الحفاظ على حالة ثابتة ريثما تُحدَّث البيانات، كما يمكننا الاستفادة من المزايا التي يقدمها BehaviorSubject والذي يغلِّف observer الأصلي الذي نتلقاه من valueChanges، ولكن ما يحدث في الحقيقة هو أنَّ BehaviorSubject يحتفظ بمصفوفة قابلة للتغيير والتي تتلقى عمليات التحديث من transferArrayItem، فكل ما عليك فعله لإصلاح هذه المشكلة هو تحديث ملف AppComponent: الملف src/app/app.component.ts: ... import { AngularFirestore, AngularFirestoreCollection } from '@angular/fire/firestore'; import { BehaviorSubject } from 'rxjs'; const getObservable = (collection: AngularFirestoreCollection<Task>) => { const subject = new BehaviorSubject<Task[]>([]); collection.valueChanges({ idField: 'id' }).subscribe((val: Task[]) => { subject.next(val); }); return subject; }; @Component(...) export class AppComponent { todo = getObservable(this.store.collection('todo')) as Observable<Task[]>; inProgress = getObservable(this.store.collection('inProgress')) as Observable<Task[]>; done = getObservable(this.store.collection('done')) as Observable<Task[]>; ... } كل ما نفعله في مقتطف الشيفرة أعلاه هو إنشاء BehaviorSubject يُصدر قيمةً في كل مرة تتغير قيمة observable المرتبطة مع التجميعة، فالآن أصبح كل شيء يعمل كما يجب لأن BehaviorSubject يعيد استخدام المصفوفة خلال استدعاءات الكشف عن حدوث تغييرات ويحدِّث فقط عندما نحصل على قيمة جديدة من Firestore. 11. نشر التطبيق كل ما علينا فعله لنشر التطبيق الخاص بنا هو تنفيذ الأمر التالي: ng deploy سوف يفعل هذا الأمر ما يلي: يبني التطبيق الخاص بك باستخدام تكوين الإنتاج وبتطبيق التحسينات خلال عملية الترجمة. ينشر التطبيق الخاص بك إلى استضافة Firebase. يعطيك عنوان URL حتى تتمكن من معاينة النتيجة النهائية. نهاية المشروع تهانينا لقد نجحت في بناء لوحة kanban باستخدام Angular وFirebase. لقد أنشأت واجهة مستخدِم بثلاثة أعمدة تمثِّل حالات المهام المختلفة، كما نفَّذت عمليتي السحب والإفلات للمهام بين الأعمدة باستخدام Angular CDK، ثمَّ بنيت استمارةً لإنشاء مهام جديدة وحرَّرت المهام الموجودة باستخدام Angular Material، وبعد ذلك تعلّمت كيفية استخدام ‎@angular/fire ونقلت حالة التطبيق الخاص بك جميعها إلى Firestore، وأخيرًا نشرت تطبيقك إلى استضافة Firebase. تذكَّر أنك نشرت التطبيق باستخدام تكوينات الاختبار ولذلك تأكد من ضبط السماحيات الصحيحة قبل نشر التطبيق الخاص بك للإنتاج، حيث يمكنك معرفة كيفية القيام بذلك من هنا، كما أنك لم تأخذ بالحسبان ترتيب المهام الفردية في الحاوية نفسها في التطبيق الذي أنشأته، حيث يمكنك استخدام حقل مخصص للترتيب في المستند الخاص بالمهمة لإضافة ذلك وترتيب المهام بناءً عليه، وبالإضافة إلى ذلك بنيت لوحة kanban من أجل مستخدِم واحد فقط، مما يعني أنه لدينا لوحة kanban واحدةً لأي شخص يستخدِم التطبيق، كما ستحتاج إلى تغيير هيكل قاعدة البيانات الخاصة بك من أجل تضمين لوحات منفصلة لمستخدِمِين آخرِين. ترجمة -وبتصرف- للمقال Building a web application with Angular and Firebase من موقع developers.google.com الرسمي. اقرأ أيضًا كيفية استعمال Angular في بناء تطبيقات الويب تهيئة بيئة تطبيقات Angular ونشرها على الويب التوجيهات (Directives) في AngularJS
  2. تُعَدّ Angular منصةً وإطارًا لبناء تطبيقات الصفحة الواحدة من جهة العميل عن طريق استخدام لغة HTML ولغة TypeScript، حيث أنَّ Angular مكتوبة باستخدام لغة TypeScript، كما تُضمِّن Angular الوظائف الأساسية والاختيارية على أساس مجموعة من مكتبات TypeScript التي تستوردها بعد ذلك إلى تطبيقاتك. تعتمد بنية تطبيق Angular على بعض المفاهيم الأساسية، حيث أنَّ اللبنات الأساسية لإطار Angular هي المكونات التي تُنظَّم ضمن NgModule في التطبيق، إذ يُعرَّف تطبيق Angular على أنه مجموعة من الوحدات المعرفة ضمن المزخرف ‎@NgModule (المزخرف هو الذي يأتي المحرف @ قبله)، كما يحتوي التطبيق دائمًا على وحدة جذر تسمى root module واحدة على الأقل تمكّن من إجراء عملية تشغيل تمهيدية للتطبيق وتدير التطبيق ككل وعادًة ما تحتوي على الكثير من الوحدات التي تقدم ميزات للتطبيق تسمى feature modules. تُعرِّف المكونات طرق العرض والتي تُعَدّ مجموعات من عناصر الشاشة التي تستطيع Angular الاختيار بينها وتعديلها وفقًا لمنطق البرنامج والبيانات الخاصة بك، وتستخدِم المكونات الخدمات التي توفر وظائفًا محددةً لا ترتبط مباشرًة بطرق العرض، حيث يمكن حقن مزودِي الخدمات في المكونات على أساس تبعيات، وهذا يجعل الشيفرة الخاصة بك معياريةً وفعالةً وقابلةً لإعادة الاستخدام. تُعَدّ الوحدات modules والمكونات conponents والخدمات services أصنافًا تستخدِم المزخرفات decorators، حيث تحدِّد هذه المزخرفات نوعها وتوفّر بيانات وصفية تخبر Angular عن كيفية استخدامها. تربط البيانات الوصفية لصنف مكوِّن هذا المكون مع قالب template يُعرِّف قسم العرض الخاص به، حيث يجمع القالب بين لغة HTML والتوجيهات في Angular والإضافات المسؤولة عن عملية التربيط التي تساعد Angular في تصيير وإخراج صفحة HTML من أجل عرضها، كما توفر البيانات الوصفية لصنف الخدمة المعلومات التي تحتاج إليها Angular لجعلها متاحةً للمكونات الأخرى عن طريق حقن التبعية dependency injection -أو DI اختصارًا. تعرّف مكونات التطبيق العديد من أقسام العرض والتي تكون مرتبةً هرميًا، ولذلك توفِّر Angular خدمة التوجيه عبر الموجه Router لمساعدتك على تحديد مسارات التنقل بين هذه الأقسام، حيث يوفر الموجه إمكانيات تنقل متطورة في المتصفح. الوحدات Modules تُعَدّ وحدات NgModules الموجودة في Angular تتمةً لوحدات ES2015 في JavaScript، إلا أنها تختلف عنها، حيث يصرح المزخرف NgModule عن سياق تجميع لمجموعة من المكونات المخصصة لمجال معين أو لسير عمل ما أو مجموعة من الإمكانيات المرتبطة ببعضها ارتباطًا وثيقًا، كما يستطيع NgModule ربط المكونات المعرفة فيه بشيفرات ذات صلة مثل الخدمات بهدف تشكيل وحدات وظيفية. يحتوي كل تطبيق Angular على وحدة جذر تسمى AppModule والتي توفر آلية التكوين المسؤولة عن تشغيل التطبيق الذي يحتوي على العديد من الوحدات الوظيفية عادًة، ويمكن أن تستورد وحدات NgModule وظائفًا من وحدات NgModule أخرى كما في وحدات JavaScript، كما تسمح بتصدير الوظائف الخاصة بها واستخدامها في وحدات أخرى، إذ يجب عليك مثلًا استيراد خدمة Router من أجل استخدام خدمة التوجيه في التطبيق الخاص بك. يساعد تنظيم الشيفرة الخاصة بك ضمن وحدات وظيفية مميزة في عملية إدارة تطوير التطبيقات المعقدة وفي تسهيل عملية التصميم من أجل إعادة الاستخدام، كما تتيح لك هذه التقنية الاستفادة من عملية التحميل الكسول lazy-loading، أي تحميل الوحدات عند الطلب لتقليل كمية الشيفرات البرمجية التي يجب تحميلها عند بدء التشغيل. المكونات components يحتوي كل تطبيق Angular على مكوِّن واحد على الأقل وهو المكوِّن الجذر الذي يربط التسلسل الهرمي للمكون بنموذج كائن المستند DOM، حيث يُعرِّف كل مكون صنفًا يحتوي على بيانات التطبيق والمنطق ويرتبط بقالب HTML يحدِّد طريقة العرض في البيئة المستهدفة، كما يحدِّد المزخرف ‎@Component()‎ الصنف الموجود أسفله مباشرًة على أساس مكوِّن ويوفِّر القالب والبيانات الوصفية الخاصة به. القوالب والتوجيهات وربط البيانات يجمع القالب بين عناصر HTML وصياغة Angular التي تستطيع تعديل عناصر HTML قبل عرضها، كما تزودنا التوجيهات الخاصة بالقالب بمنطق عمل البرنامج، في حين يربط ترميز التربيط بيانات التطبيق الخاص بك مع DOM. يوجد نوعان من صياغة الربط: يتيح لك التربيط عبر الحدث الاستجابة لمدخلات المستخدِم في البيئة المستهدَفة عن طريق تحديث بيانات التطبيق الخاص بك. يتيح لك التربيط عبر الخاصية إدخال القيم المحسوبة من بيانات التطبيق الخاص بك إلى عناصر HTML. تقيّم Angular التوجيهات وتحل السياق المرتبط بالقالب لتعديل عناصر HTML وDOM وفقًا لبيانات برنامجك والمنطق قبل إظهار قسم العرض، كما تدعم Angular تربيط البيانات باتجاهَين، مما يعني انعكاس التغييرات التي تحدث في DOM مثل خيارات المستخدِم في بيانات البرنامج الخاص بك أيضًا، كما يمكن للقوالب الخاصة بك استخدام الأنابيب من أجل تحسين تجربة المستخدِم عن طريق تحويل القيم قبل عرضها، حيث يمكنك مثلًا استخدام الأنابيب لعرض التواريخ وقيم العملات عرضًا يناسب البيئة المحلية للمستخدِم، إذ توفِّر Angular أنابيبًا pipes معرّفةً مسبقًا من أجل إجراء تحويلات عليها ويمكنك تعريف الأنابيب الخاصة بك أيضًا. الخدمات وحقن التبعية إذا أدرت مشاركة بيانات أو منطق ما غير مرتبط بقسم عرض معين عبر المكونات فبإمكانك إنشاء صنف خدمة بحيث يُسبق التعريف الخاص بصنف الخدمة بالمزخرف ‎@Injectable()‎، حيث يوفِّر هذا المزخرِف البيانات الوصفية التي تمكّن مزودي الخدمة الآخرين من حقنهم على أساس تبعيات في الصنف الخاص بك، ويتيح لك حقن التبعية DI الحفاظ على أصناف المكونات الخاصة بك بصورة بسيطة وفعالة، حيث أنَّ المكونات لا تجلب البيانات من الخادم أو تتحقق من صحة إدخال المستخدِم أو تطبع على الطرفية مباشرةً، ولإنما تفوِّض هذه المهام إلى الخدمات. التوجيه يوفِّر لك وحدات Angular الجاهزة خدمة التوجيه عبر الوحدة Router التي تتيح لك تعريف وجهات محددة في تطبيقك والتنقل في مختلف مساراتها مهما كانت متداخلة وعرض القوالب المعرَّفة في تلك المسارات حيث نمذجت واجهة التنقل Router لتعمل بآلية عمل التنقل نفسه والخاص بالمتصفِّحات المعروفة: أدخِل عنوان URL في شريط العنوان وسينقلك المتصفِّح إلى الصفحة التابعة لهذا العنوان. انقر على الروابط في الصفحة وسينقلك المتصفِّح إلى صفحة أخرى جديدة. انقر على أزرار العودة إلى الخلف أو الانتقال إلى الأمام الخاصة بالمتصفح وسينقلك المتصفِّح إلى الوراء أو إلى الأمام اعتمادًا على تاريخ الصفحات التي زرتها. يعيِّن جهاز التوجيه مسارات تشبه عناوين URL إلى أقسام العرض بدلًا من الصفحات وعندما يجري المستخدِم إجراءً معيَّنًا مثل النقر على رابط ما والذي يحمِّل صفحةً جديدةً في المتصفح، فيعترض هنا جهاز التوجيه سلوك المتصفح ويعرض له أو يخفي عنه التسلسل الهرمي للعرض، فإذا حدَّد جهاز التوجيه أن حالة التطبيق الحالية تتطلب وظائفًا معينةً ولم تُحمَّل الوحدة التي تحدِّد هذه الوظائف، فيمكن لجهاز التوجيه تحميل الوحدة تحميلًا بطيئًا عند الطلب. يعترض جهاز التوجيه عنوان URL للرابط وفقًا لقواعد التنقل التي حددتها لأقسام العرض الخاصة بتطبيقك وحالة البيانات، حيث يمكنك الانتقال إلى طرق عرض جديدة عندما ينقر المستخدِم على زر أو يختار أحد الخيارات الموجودة في قائمة منسدلة أو في استجابة لبعض التنبيهات القادمة من أي مصدر آخر، حيث يسجِّل جهاز التوجيه النشاط في تاريخ المتصفِّح، وبالتالي تعمل أزرار العودة إلى الخلف أو الانتقال إلى الأمام خلال عملية التنقل أيضًا. يجب عليك ربط مسارات التنقل بمكوناتك من أجل تعريف قواعد التنقل، حيث يستخدِم المسار بنيةً تشبه عنوان URL، وقد يدمج به بيانات التطبيق الخاصة بك بالطريقة نفسها التي تدمج بها بنية القالب أقسام العرض الخاصة بك مع بيانات تطبيقك، كما يمكنك بعد ذلك تطبيق منطق معين في البرنامج لاختيار أي من أقسام العرض يجب إظهارها أو إخفاؤها على أساس استجابة لإدخال المستخدِم وقواعد الوصول التي عرَّفتها. الخاتمة تعلمت الأساسيات حول اللبنات الأساسية التي يتكون منها تطبيق Angular، ويوضِّح الرسم البياني التالي كيف ترتبط هذه المكونات الأساسية مع بعضها. يعرِّف كل من المكوِّن والقالب قسم العرض في Angular. يضيف المزخرِف إلى صنف المكوِّن بيانات وصفيةً تتضمن مؤشرًا إلى القالب المرتبط بهذا المكوِّن. تعدِّل التوجيهات وصياغة التربيط في قالب المكون قسم العرض استنادًا إلى بيانات التطبيق والمنطق. يوفِّر حاقن التبعية الخدمات لمكوِّن ما مثل خدمة الموجِّه التي تتيح لك تعريف التنقل بين أقسام العرض. أصبحت الآن جاهزًا لتتعلم المزيد عن الأدوات والتقنيات المتاحة لمساعدتك في إنشاء تطبيقات Angular ونشرها. ترجمة -وبتصرف- للمقال Introduction to Angular concepts من موقع angular.io الرسمي. اقرأ أيضًا كيفية استعمال Angular في بناء تطبيقات الويب إضافة التنقل وإدارة البيانات في تطبيق Angular مبادئ AngularJS
  3. سنتعلم في هذا المقال كيفية تهيئة البيئة الأولية لتطبيقات Angular وإعدادها وتوليد ملفات التطبيق الأساسية عبر واجهة سطر أوامر Angular المعروفة بالاسم Angular CLI ثم سنشرح بعدها كيفية تهيئة تطبيق Angular للنشر على إحدى الاستضافات أو الخوادم. سنستفيد من هذا المقال في تعلم كيفية نشر تطبيق التجارة الإلكترونية الذي بنيناه في المقالات السابقة، وسنبدأ في الخطوة الأولى في تعلم كيفية إنشاء بيئة محلية وتوليد الملفات الأولية الأساسية لأي تطبيق Angular. إن لم تكون تابعت تلك المقالات، فننصح بالرجوع إليها وقراءتها وتطبيقها أولًا ثم إكمال قراءة هذا المقال الذي يعتمد عليها. يُعَدّ هذا المقال جزءًا من سلسلة مقالات حول بناء تطبيق تجارة إلكترونية عبر Angular: ما هي Angular؟ كيفية استعمال Angular في بناء تطبيقات الويب إضافة التنقل وإدارة البيانات في تطبيق Angular تهيئة بيئة تطبيقات Angular ونشرها على الويب إعداد البيئة المحلية ومساحة العمل يشرح هذا القسم كيفية إعداد بيئتك الخاصة بالتطوير باستخدام Angular عن طريق أداة Angular CLI والتي تتضمن معلومات حول المتطلبات الأساسية وطريقة تثبيت CLI وإنشاء مساحة عمل أولية وتطبيق أولي وتشغيل هذا التطبيق محليًا للتحقق من صحة إعداد البيئة. المتطلبات الأساسية يجب أن تكون على معرفة بما يلي لكي تتمكن من استخدام إطار Angular: JavaScript HTML CSS تُعَدّ معرفة TypeScript مفيدةً أيضًا ولكنها غير مطلوبة، وتحتاج إلى المتطلبات التالية لتثبيت Angular على نظامك المحلي: Node.js يتطلب Angular نسخةً طويلة الدعم ومستقرةً من Node.js. يمكنك الاطلاع على nodejs.org لمزيد من المعلومات حول تثبيت Node.js، وإذا كنت غير متأكد من إصدار Node.js الذي يعمل على النظام لديك، فشغِّل الأمر node -v في نافذة الطرفية فقط. مدير حزم npm تعتمد كل من Angular CLI وتطبيقات Angular على مدير الحزم npm من أجل استخدام العديد من الميزات والوظائف، وتحتاج أنت إلى مدير حزمة npm لتحميل وتثبيت حزم npm، كما يستخدم هذا الدليل واجهة سطر أوامر لعميل npm والتي تُثَبَّت مع Node.js افتراضيًا، ويمكنك تشغيل الأمر npm -v في نافذة طرفية لتتحقق من تثبيت عميل npm على جهازك. تثبيت واجهة سطر الأوامر Angular CLI يمكنك استخدام Angular CLI لإنشاء المشاريع وتوليد شيفرات التطبيق واستيراد المكتبات، وتنفيذ مجموعة متنوعة من المهام المتعلقة بعملية التطوير، مثل الاختبار والتجميع والنشر، لذا افتح نافذةً طرفيةً ما ونفِّذ الأمر التالي لتثبيت Angular CLI: npm install -g @angular/cli إنشاء مساحة عمل وتوليد الملفات الأولية لتطبيق Angular يمكنك تطوير التطبيقات الخاصة بك ضمن مساحة عمل Angular، ولإنشاء مساحة عمل جديدة وتطبيق أولي اتبع ما يلي: أولًا، نفِّذ الأمر التالي ng new واختر الاسم my-app على أساس اسم للمشروع كما هو موضَّح: ng new my-app ثانيًا، يطالبك الأمر ng new بمعلومات حول الميزات التي تريد إضافتها للتطبيق الأولي، كما اقبل بالإعدادات الافتراضية عن طريق الضغط على مفتاح Enter أو Return. يثبِّت سطر أوامر Angular حزم npm الضرورية لعمل Angular والتبعيات الأخرى حيث من الممكن أن تستغرق هذه العملية بضع دقائق، وتنشئ واجهة سطر الأوامر CLI مساحة عمل جديدة وتطبيق ترحيب بسيط جاهز للعمل. تشغيل تطبيق Angular يتضمن Angular CLI خادم لتتمكن من بناء وتخديم التطبيق الخاص بك محليًا. انتقل إلى مجلد مساحة العمل مثل my-app. نفِّذ الأمر التالي: cd my-app ng serve --open يُشغِّل الأمر ng serve الخادم ومراقبة ملفاتك وإعادة بناء تطبيقك في حال حدوث تغييرات على هذه الملفات، كما يفتح ‎--open (أو فقط ‎-o) متصفحك تلقائيًا ويوجّهك إلى العنوان http://localhost:4200/‎، وإذا قمت بعملية التثبيت والإعداد بصورة صحيحة، فستشاهد صفحةً مشابهةً للصفحة التالية على شاشة المتصفح الخاص بك. لنأخذ الآن مثالًا عمليًا ونشغل تطبيق التجارة الإلكترونية الذي بنيناه في مقالات سابقة، إذ بنينا التطبيق على منصة StackBlitz من هذه الملفات الأولية. لنبدأ أولًا بتشغيل التطبيق الخاص بك محليًا باتباع الخطوات التالية: انقر على أيقونة تنزيل المشروع الموجودة في القائمة اليسارية مقابل كلمة مشروع Project لتنزيل شيفرة المصدر من مشروع StackBlitz الخاص بك الذي عملت عليه في المقالات السابقة. أنشأ مساحة عمل جديدة عن طريق الأمر ng new كما تعلمت سابقًا، حيث أنَّ my-project-name هو الاسم الذي تريد تسمية مشروعك به: ng new my-project-name يعرض هذا الأمر مجموعة من الأسئلة التي تدخل في عملية ضبط التطبيق وسنقوم في هذا المقال بالإبقاء على الإعدادات الافتراضية لكل سؤال. استبدل مجلد ‎/src في تطبيقك الذي قمت بإنشائه للتو بمجلد ‎/src الذي قمت بتنزيله من StackBlitz. ثانيًا، استخدم أمر CLI التالي لتشغيل التطبيق محليًا: ng serve ثالثًا، انتقل إلى العنوان http://localhost:4200 لاستعراض التطبيق الخاص بك في المتصفح، فإذا لم يكن المنفذ الافتراضي 4200 متوفرًا، فيمكنك تحديد منفذ آخر عن طريق استخدام راية المنفذ كما في المثال التالي: ng serve --port 4201 يمكنك تحرير الشيفرة ورؤية التغييرات أثناء تخديم التطبيق الخاص بك عن طريق عملية التحديث التلقائي التي يقوم بها المتصفح، كما يمكنك إيقاف أمر ng serve فقط بالضغط على الاختصار Ctrl+ c. بناء تطبيق Angular ونشره على الويب وصلنا إلى المرحلة الأخيرة وهي بناء تطبيق Angular ونشره على الإنترنت. أولًا، استخدم الأمر build لبناء التطبيق الخاص بك لعملية الإنتاج، حيث يستخدِم هذا الأمر إعدادات البناء الخاصة ببيئة الإنتاج production. ng build يُنشئ هذا الأمر مجلد dist في الدليل الجذر للتطبيق، حيث يحوي جميع الملفات التي تحتاجها خدمة الاستضافة لتخديم التطبيق الخاص بك. ثانيًا، انسخ محتويات المجلد dist/my-project-name إلى خادم الويب الخاص بك، ونظرًا لأن هذه الملفات ثابتة، فيمكنك استضافتها على أي خادم ويب قادر على تخديم مثل هذا النوع من الملفات مثل Node.js أو Java أو ‎.NET أو أي واجهة خلفية مثل Firebase أو Google Cloud أو App Engine. الخطوة التالية أعددنا في هذا المقال بيئةً محليةً لتطبيق Angular، وتعلّمنا كيفية تشغيل التطبيق لبدء عملية تطويره، ثم تعلمنا في النهائية كيفية بناء التطبيق وتهيئته للنشر، كما طبَّقنا ذلك على تطبيق التجارة الإلكترونية الذي بنيناه سابقًا، وأصبحت الآن تملك فكرةً عامةً عن Angular وأصبحت جاهزًا لبدء احتراف برمجة تطبيقات Angular. ترجمة -وبتصرف- للمقال Deploying an application والمقال Setting up the local environment and workspace من موقع angular.io الرسمي. اقرأ أيضًا مقدمة في مفاهيم Angular إضافة التنقل وإدارة البيانات في تطبيق Angular كيفية استعمال Angular في بناء تطبيقات الويب تعلم البرمجة
  4. بُني هذا المقال على الخطوة الأولى من سلسلة المقالات التعليمية لتعلم استعمال Angular عبر بناء تطبيق تجارة إلكترونية، حيث يحتوي تطبيق المتجر عبر الانترنت في هذه المرحلة من التطوير على قائمة المنتجات الأساسية، كما ستضيف في الأقسام التالية الميزات التالية إلى التطبيق: اكتب عنوان URL في شريط العنوان للانتقال إلى صفحة المنتج المقابلة له. انقر فوق الروابط الموجودة في الصفحة للتنقل داخل تطبيق الصفحة الواحدة الخاص بك. انقر على زرَّي الانتقال للخلف والأمام للمتصفح للتنقل في سجل المتصفح تلقائيًا. تحديث قسم العرض الخاص بتفاصيل المنتج ليتضمن زر الشراء الذي يضيف المنتج الحالي إلى قائمة منتجات تديرها خدمة سلة التسوق. إضافة مكوِّن السلة الذي يعرض العناصر الموجودة في السلة. إضافة مكوِّن الشحن الذي يعيد أسعار الشحن للعناصر الموجودة في السلة باستخدام HttpClient الخاص بـ Angular لاستعادة بيانات الشحن من ملف json.. هذه المقال جزءٌ من سلسلة مقالات حول بناء تطبيق تجارة إلكترونية عبر Angular: ما هي Angular؟ كيفية استعمال Angular في بناء تطبيقات الويب إضافة التنقل وإدارة البيانات في تطبيق Angular تهيئة بيئة تطبيقات Angular ونشرها على الويب ربط مسار URL مع مكون يستخدم التطبيق الموجِّه Router من Angular للانتقال إلى المكوِّن ProductListComponent افتراضيًا، حيث يوضِّح هذا القسم كيفية تعريف الوجهة لعرض تفاصيل المنتجات بصورة فردية. نبدأ أولًا بتوليد مكون جديد باسم product-details لعرض تفاصيل المنتج وذلك بتنفيذ الأمر التالي: ng generate component product-details بعد ذلك أضف وجهة لتفاصيل المنتج في app.module.ts من خلال استخدام path مع القيمة products/:productId وضبط component إلى المكون ProductDetailsComponent. الملف src/app/app.module.ts: @NgModule({ imports: [ BrowserModule, ReactiveFormsModule, RouterModule.forRoot([ { path: '', component: ProductListComponent }, { path: 'products/:productId', component: ProductDetailsComponent }, ]) ], افتح product-list.component.html، وعدِّل المِربط الخاص باسم المنتج ليتضمن routerLink مع استخدام product.id على أساس معامِل. الملف src/app/product-list/product-list.component.html: <div *ngFor="let product of products"> <h3> <a [title]="product.name + ' details'" [routerLink]="['/products', product.id]"> {{ product.name }} </a> </h3> <!-- . . . --> </div> يساعدك الموجِّه RouterLink على تخصيص الرابط، حيث يحتوي الموجه أو العنوان في هذه الحالة على جزء واحد ثابت هو ‎/products، ويكون الجزء الأخير متغيرًا يتمثَّل في إدراج الخاصية id للمنتج الحالي، حيث سيكون العنوان URL مثلًا لمنتج يملك المُعرِّف 1 مشابهًا للعنوان https://getting-started-myfork.stackblitz.io/products/1 أخيرًا، تحقّق من عمل الموجِّه على النحو المطلوب من خلال النقر على اسم المنتج، حيث يجب أن يعرض التطبيق المكوِّن ProductDetailsComponent والذي سيعرض رسالة "product-details works" أي صفحة تفاصيل المنتج تعمل!، ولاحظ أنَّ العنوان في نافذة المعاينة يتغير، كما يكون الجزء الأخير هو products/#‎ حيث تمثّل # رقم المسار الذي نقرت عليه. عرض تفاصيل المنتج يعالِج ProductDetailsComponent طريقة عرض كل منتج، يعرض موجِّه Angular المكوِّنات اعتمادًا على عنوان المتصفح والمسارات التي حددتها، كما ستستخدِم في هذا القسم موجِّه Angular لدمج بيانات المنتجات products ومعلومات المسار لعرض التفاصيل الخاصة لكل منتج. بدايةً، استورد ActivatedRoute من ‎@angular/router ومصفوفة المنتجات products من ‎../products في ملف product-details.component.ts. الملف src/app/product-details/product-details.component.ts: import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Product, products } from '../products'; حدِّد خاصية product. الملف src/app/product-details/product-details.component.ts: export class ProductDetailsComponent implements OnInit { product: Product|undefined; /* ... */ } احقن ActivatedRoute في الباني constructor()‎ عن طريق إضافة private route: ActivatedRoute على أساس وسيط ضمن أقواس الباني. الملف src/app/product-details/product-details.component.ts: export class ProductDetailsComponent implements OnInit { product: Product|undefined; constructor(private route: ActivatedRoute) { } } يوجد صنف يدعى ActivatedRoute لكل مكوِّن يحمِّله موجّه Angular بحيث يحتوي على معلومات حول الموجه route ومعامِلاته، كما تُهيّئ المكوِّن ليستخدِم خدمة عن طريق حقن ActivatedRoute، وسنغطي ذلك في خطوة إدارة البيانات بتفصيل أكثر. استخرج productId من معامِلات المسار في دالة ngOnInit()‎ واعثر على المنتج المقابل له ضمن مصفوفة products. الملف src/app/product-details/product-details.component.ts: ngOnInit() { // First get the product id from the current route. const routeParams = this.route.snapshot.paramMap; const productIdFromRoute = Number(routeParams.get('productId')); // Find the product that correspond with the id provided in route. this.product = products.find(product => product.id === productIdFromRoute); } تقابل معامِلات المسار متغيرات المسار التي حدَّدتها في المسار، حيث نستخدِم route.snapshot للوصول إلى معاملات الموجه والذي يُعَدّ ActivatedRouteSnapshot ويحوي معلومات حول الموجه النشط في لحظة معينة من الزمن، كما يوفِّر العنوان الذي يطابق موجه productId، حيث يستخدِم Angular معرف المنتج productId لعرض تفاصيل كل منتج فريد. حدِّث قالب ProductDetailsComponent لعرض تفاصيل المنتج باستخدام ‎*ngIf، فإذا كان المنتج منتج موجودًا، فسيعرض محتوى <div> مع اسم وسعر وتوصيف المنتج. الملف src/app/product-details/product-details.component.html <h2>Product Details</h2> <div *ngIf="product"> <h3>{{ product.name }}</h3> <h4>{{ product.price | currency }}</h4> <p>{{ product.description }}</p> </div> يستخدِم السطر <h4>{{ product.price | currency }}</h4> الأنابيب العملة currency لتحويل قيمة product.price من عدد إلى سلسلة نصية بصيغة عملة، حيث يُعَدّ الأنبوب طريقةً يمكنك من خلالها تحويل البيانات الموجودة في قالب HTML الخاص بك. عندما ينقر المستخدمون على اسم في قائمة المنتجات، ينقلهم الموجِّه إلى العنوان المميز للمنتج ويعرِض ProductDetailsComponent ويعرض تفاصيل المنتج. يحتوي تطبيق المتجر في هذه المرحلة من التطوير على فئات المنتجات مع قسمي عرض هما قائمة المنتجات وتفاصيل المنتج، حيث يمكن للمستخدِمين النقر على اسم المنتج من قائمة المنتجات لمشاهدة تفاصيله في قسم عرض جديد مع رابط مميز أو مسار، وسنكمل إضافة ميزة سلة التسوق وإدارة بياناتها للتطبيق. إنشاء خدمة سلة التسوق تُعَدّ الخدمة service في Angular نسخةً من صنف تجعله متاحًا لأي جزء من أجزاء التطبيق الخاص بك باستخدام نظام حقن التبعية في Angular، كما يمكن للمستخدمين حاليًا عرض معلومات المنتج، ويمكن للتطبيق أن يحاكي المشاركة والإشعارات في حال حدوث تغييرات في المنتج، وتكون الخطوة التالية هي بناء طريقة تمكِّن المستخدِمين من إضافة منتجات إلى السلة، حيث يرشدك هذا المقال من خلال عملية إضافة زر الشراء وإعداد خدمة السلة لتخزين المعلومات حول المنتجات ضمن السلة. تعريف خدمة السلة لإنشاء خدمة السلة، استعمل الطرفية ونفذ الأمر التالي لتوليد خدمة باسم cart: ng generate service cart الملف src/app/cart.service.ts: import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class CartService { constructor() {} } استورد واجهة Product من ‎./products.ts ضمن cart.service.ts، وعرّف خاصيةً باسم items في صنف CartService لتخزين مصفوفة المنتجات الحالية الموجودة في سلة التسوق. الملف src/app/cart.service.ts: import { Product } from './products'; /* . . . */ export class CartService { items: Product[] = []; /* . . . */ } عرِّف التوابع المسؤولة عن إضافة عناصر إلى سلة التسوق وإعادة عناصر سلة التسوق ومسح عناصر سلة التسوق. الملف src/app/cart.service.ts: export class CartService { items: Product[] = []; /* . . . */ addToCart(product: Product) { this.items.push(product); } getItems() { return this.items; } clearCart() { this.items = []; return this.items; } /* . . . */ } شرح التوابع السابقة المعرفة: addToCart()‎: يضيف منتجًا إلى مصفوفة items. getItems()‎: يجمِّع العناصر التي يضيفها المستخدمون إلى سلة التسوق ويُعيد كل عنصر مع الكمية المرتبطة به. clearCart()‎: يعيد مصفوفةً فارغةً من العناصر، أي أنه يُفرِّغ السلة من العناصر الموجودة فيها. استخدم خدمة السلة سنبرمج الآن عملية إضافة منتج إلى السلة عن طريق استخدام الخدمة CartService. أولًا، استورد خدمة السلة في ملف product-details.component.ts. الملف src/app/product-details/product-details.component.ts: import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Product, products } from '../products'; import { CartService } from '../cart.service'; ثانيًا، احقن خدمة السلة عن طريق إضافتها إلى الباني constructor()‎. الملف src/app/product-details/product-details.component.ts: export class ProductDetailsComponent implements OnInit { constructor( private route: ActivatedRoute, private cartService: CartService ) { } } ثالثًا، عرِّف التابع addToCart()‎ الذي تضيف المنتج الحالي إلى سلة التسوق. الملف src/app/product-details/product-details.component.ts: export class ProductDetailsComponent implements OnInit { addToCart(product: Product) { this.cartService.addToCart(product); window.alert('Your product has been added to the cart!'); } } يفعل التابع addToCart()‎ ما يلي: يأخذ المنتج الحالي product على أساس وسيط. يستخدِم التابع addToCart()‎ الموجود ضمن CartService لإضافة المنتج إلى السلة. يعرض رسالةً تخبرنا بأنه قد أُضيف منتج إلى السلة. رابعًا، أضف زر الشراء في ملف product-details.component.html واربط الحدث click()‎ إلى التابع addToCart()‎، إذ تحدِّث هذه الشيفرة قالب تفاصيل المنتج بإضافة زر الشراء المسؤول عن إضافة المنتج الحالي إلى سلة التسوق. الملف src/app/product-details/product-details.component.html: <h2>Product Details</h2> <div *ngIf="product"> <h3>{{ product.name }}</h3> <h4>{{ product.price | currency }}</h4> <p>{{ product.description }}</p> <button (click)="addToCart(product)">Buy</button> </div> خامسًا، تحقّق من ظهور زر الشراء الجديد كما هو متوقَّع من خلال تحديث التطبيق والنقر على اسم المنتج لعرض تفاصيله. أخيرًا، انقر على زر الشراء لإضافة المنتج إلى قائمة العناصر المخزنة في سلة التسوق وعرض رسالة تأكيد. إنشاء قسم العرض الخاص بالسلة يمكنك إنشاء قسم العرض الخاص بسلة التسوق لكي يتمكن العملاء من رؤية سلة التسوق الخاصة بهم وذلك بخطوتين: أنشئ مكوِّن السلة وهيّئ التوجيه إلى هذا المكوِّن الجديد. اعرض العناصر الموجودة في السلة. إعداد مكون السلة اتبع الخطوات نفسها التي استخدمتها في إنشاء ProductDetailsComponent من أجل إنشاء قسم العرض الخاص بالسلة وهيِّئ التوجيه لهذا المكون الجديد. بدايةً، أنشئ مكونًا جديدًا باسم cart يمثل السلة بتنفيذ الأمر التالي في الطرفية: ng generate component cart سيولد هذا الأمر الملف cart.component.ts الخاص بالمكون وقالبه وملفات تنسيقاته. الملف src/app/cart/cart.component.ts: import { Component } from '@angular/core'; @Component({ selector: 'app-cart', templateUrl: './cart.component.html', styleUrls: ['./cart.component.css'] }) export class CartComponent { constructor() { } } ينشئ StackBlitz -الذي ضبطناه في المقال الأول ببداية المشروع- أيضًا تابع ngOnInit()‎ في المكونات افتراضيًا، ويمكنك تجاهل ngOnInit()‎ الخاصة بالمكون CartComponent في هذا المقال. لاحظ أن المكون CartComponent المولد حديثًا يضاف إلى قسم التصريحات declarations في الملف app.module.ts. الملف src/app/app.module.ts: import { CartComponent } from './cart/cart.component'; @NgModule({ declarations: [ AppComponent, TopBarComponent, ProductListComponent, ProductAlertsComponent, ProductDetailsComponent, CartComponent, ], أبقَ في الملف app.module.ts وأضف فيه وجهة لمكون CartComponent، مع تعيين قيمة path إلى القيمة cart. الملف src/app/app.module.ts: @NgModule({ imports: [ BrowserModule, ReactiveFormsModule, RouterModule.forRoot([ { path: '', component: ProductListComponent }, { path: 'products/:productId', component: ProductDetailsComponent }, { path: 'cart', component: CartComponent }, ]) ], حدِّث زر الدفع بحيث يوجِّه إلى الرابط ‎/‎‎cart، وأضف موجِّه routerLink يشير إلى ‎/cart في ملف top-bar.component.html. الملف src/app/top-bar/top-bar.component.html: <a routerLink="/cart" class="button fancy-button"> <i class="material-icons">shopping_cart</i>Checkout </a> تحقّق من عمل CartComponent كما هو متوقَّع من خلال النقر على زر الدفع، حيث يمكنك رؤية النص الافتراضي الذي وضعناه "السلة تعمل!"، ويملك الرابط النمط https://getting-started.stackblitz.io/cart، حيث أنَّ getting-started.stackblitz.io قد تكون مختلفةً في مشروع StackBlitz الخاص بك. عرض عناصر السلة سنعمل في هذا القسم على عرض المنتجات في السلة عن طريق استخدام خدمة السلة. بدايةً، استورد CartService في ملف cart.component.ts من ملف cart.service.ts. الملف src/app/cart/cart.component.ts: import { Component } from '@angular/core'; import { CartService } from '../cart.service'; احقن الخدمة CartService بحيث يتمكن المكون CartComponent من استخدامه عن طريق إضافته إلى الباني constructor()‎. الملف src/app/cart/cart.component.ts: export class CartComponent { constructor( private cartService: CartService ) { } } عرِّف الخاصية items لتخزين المنتجات في السلة. الملف src/app/cart/cart.component.ts: export class CartComponent { items = this.cartService.getItems(); constructor( private cartService: CartService ) { } } تحدد هذه الشيفرة العناصر عن طريق تابع getItems()‎ الموجودة ضمن CartService، ولقد عرَّفت هذا التابع عندما أنشأت cart.service.ts. حدِّث قالب السلة مع إضافة ترويسة، واستخدم <div> مع ‎*ngFor لعرض كل عنصر من عناصر السلة مع اسمه وسعره، وستكون شيفرة قالب المكون CartComponent كما يلي: الملف src/app/cart/cart.component.html: <h3>Cart</h3> <div class="cart-item" *ngFor="let item of items"> <span>{{ item.name }}</span> <span>{{ item.price | currency }}</span> </div> تحقّق من عمل السلة كما يجب: انقر على My Store. انقر على اسم المنتج لعرض تفاصيله. انقر زر الشراء لإضافة المنتج إلى السلة. انقر فوق زر الدفع لرؤية السلة. جلب أسعار الشحن تُعيد الخوادم البيانات على هيئة مجرى stream غالبًا والتي تُعَدّ مفيدةً لتسهيلها من عملية نقل البيانات المعادة وإجراء تعديلات على الطريقة التي تُطلَب فيها تلك البيانات، كما تعد الوحدة HttpClient في Angular طريقة أساسية لجلب البيانات من واجهات برمجة تطبيقات خارجية وتوفيرها للتطبيق الخاص بك على أساس مجرى، حيث يُوضِّح هذا القسم لك كيفية استخدام الوحدة HttpClient لاسترجاع أسعار الشحن من ملف خارجي. يأتي التطبيق الذي ولَّده StackBlitz مع بيانات الشحن بشكل مسبق ضمن ملف assets/shipping.json، كما يمكنك استخدام هذه البيانات لإضافة أسعار الشحن للعناصر الموجودة في السلة. الملف src/assets/shipping.json: [ { "type": "Overnight", "price": 25.99 }, { "type": "2-Day", "price": 9.99 }, { "type": "Postal", "price": 2.99 } ] تهيئة AppModule لاستخدام HttpClient يجب عليك ضبط التطبيق الخاص بك لاستخدام الوحدة HttpClientModule من أجل استخدام خدمة HttpClient من Angular، حيث يُسجِّل HttpClientModule المزوِّدين الذين يحتاجهم التطبيق الخاص بك لكي يستخدم خدمة HttpClient في جميع ملفات التطبيق. لنبدأ، أولًا استورد الوحدة HttpClientModule في ملف app.module.ts عن طريق حزمة ‎@angular/common/http في الجزء العلوي من الملف مع باقي الاستيرادات، ونظرًا لوجود عدد من الاستيرادات الأخرى، فإن هذه الشيفرة تحذفها للإيجاز، ولذلك تأكد من وضع الاستيرادات الموجودة في مكانها الصحيح. الملف src/app/app.module.ts: import { HttpClientModule } from '@angular/common/http'; ثانيًا، لتسجيل مزوِّد HttpClient ليكون متاحًا في جميع ملفات التطبيق، أضف HttpClientModule إلى ملف AppModule ضمن مصفوفة الاستيرادات imports في ‎@NgModule. الملف src/app/app.module.ts: @NgModule({ imports: [ BrowserModule, HttpClientModule, ReactiveFormsModule, RouterModule.forRoot([ { path: '', component: ProductListComponent }, { path: 'products/:productId', component: ProductDetailsComponent }, { path: 'cart', component: CartComponent }, ]) ], declarations: [ AppComponent, TopBarComponent, ProductListComponent, ProductAlertsComponent, ProductDetailsComponent, CartComponent, ], bootstrap: [ AppComponent ] }) export class AppModule { } تهيئة الخدمة CartService لاستخدام HttpClient تتمثَّل الخطوة التالية في حقن خدمة HttpClient ضمن خدمتك حتى يتمكن تطبيقك من جلب البيانات والتفاعل مع الموارد وواجهات برمجة التطبيقات الخارجية. استورد خدمة HttpClient من حزمة ‎@angular/common/http ضمن ملف cart.service.ts. الملف src/app/cart.service.ts: import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Product } from './products'; احقن HttpClient في الباني constructor()‎ الخاص بالخدمة CartService. الملف src/app/cart.service.ts: export class CartService { items: Product[] = []; constructor( private http: HttpClient ) {} /* . . . */ } تهيئة الخدمة CartService لجلب أسعار الشحن يمكنك استخدام تابع get()‎ من HttpClient للحصول على بيانات الشحن من ملف shipping.json. عرِّف تابعًا جديدًا getShippingPrices()‎ يستخدِم تابع get()‎ من HttpClient ضمن ملف cart.service.ts أسفل تابع clearCart()‎. الملف src/app/cart.service.ts: export class CartService { /* . . . */ getShippingPrices() { return this.http.get<{type: string, price: number}[]>('/assets/shipping.json'); } } انتهينا من عملية الضبط والتهيئة لجلب أسعار الشحن. إنشاء مكون الشحن الآن بعدما هيّئت تطبيقك لاسترجاع بيانات الشحن، يمكنك إنشاء مكان لعرض تلك البيانات. بدايةً، ولِّد مكوِّنًا باسم shipping يمثل عملية الشحن بتنفيذ الأمر التالي في الطرفية: ng generate component shipping سيولد هذا الأمر الملف shipping.component.ts الخاص بالمكون وقالبه وملفات تنسيقاته. الملف src/app/shipping/shipping.component.ts: import { Component } from '@angular/core'; @Component({ selector: 'app-shipping', templateUrl: './shipping.component.html', styleUrls: ['./shipping.component.css'] }) export class ShippingComponent { constructor() { } } أضف مسار للشحن في ملف app.module.ts، واضبط الحقل pah إلى القيمة shipping والحقل component إلى المكون ShippingComponent. الملف src/app/app.module.ts: @NgModule({ imports: [ BrowserModule, HttpClientModule, ReactiveFormsModule, RouterModule.forRoot([ { path: '', component: ProductListComponent }, { path: 'products/:productId', component: ProductDetailsComponent }, { path: 'cart', component: CartComponent }, { path: 'shipping', component: ShippingComponent }, ]) ], declarations: [ AppComponent, TopBarComponent, ProductListComponent, ProductAlertsComponent, ProductDetailsComponent, CartComponent, ShippingComponent ], bootstrap: [ AppComponent ] }) export class AppModule { } لا يوجد رابط لمكون الشحن الجديد حتى الآن، ولكن يمكنك رؤية قالبه في جزء المعاينة عن طريق إدخال عنوان URL الذي يحدده مساره، حيث يحتوي عنوان URL على النمط الآتي: https://angular-ynqttp--4200.local.webcontainer.io/shipping حيث أنَّ جزء angular-ynqttp--4200.local.webcontainer.io قد يكون مختلفًا في مشروع StackBlitz الخاص بك. تهيئة المكون ShippingComponent لاستخدام الخدمة CartService ينضبط المكون ShippingComponent لجلب بيانات الشحن عبر استخدام طلب HTTP من ملف shipping.json. أولًا، استورد CartService في ملف shipping.component.ts. الملف src/app/shipping/shipping.component.ts: import { Component } from '@angular/core'; import { CartService } from '../cart.service'; ثانيًا، احقن خدمة السلة في الباني constructor()‎ الخاص بالمكون ShippingComponent. الملف src/app/shipping/shipping.component.ts: constructor(private cartService: CartService) { } ثالثًا، عرِّف الخاصية shippingCosts التي تُضبط باستخدام التابع getShippingPrices()‎ من الخدمة CartService. الملف src/app/shipping/shipping.component.ts: export class ShippingComponent { shippingCosts = this.cartService.getShippingPrices(); } رابعًا، حدِّث قالب المكون ShippingComponent لكي يعرض أنواع وأسعار الشحن باستخدام أنبوب async. الملف src/app/shipping/shipping.component.html: <h3>Shipping Prices</h3> <div class="shipping-item" *ngFor="let shipping of shippingCosts | async"> <span>{{ shipping.type }}</span> <span>{{ shipping.price | currency }}</span> </div> يُعيد أنبوب async أحدث قيمة من مجرى البيانات ويتابع تحميل مكوِّن معيَّن، حيث يتوقف أنبوب async تلقائيًا عندما تدمر Angular هذا المكوِّن. خامسًا، أضف رابطًا من قسم العرض الخاص بالمكون CartComponent إلى قسم العرض الخاص بالمكون ShippingComponent. الملف src/app/cart/cart.component.html: <h3>Cart</h3> <p> <a routerLink="/shipping">Shipping Prices</a> </p> <div class="cart-item" *ngFor="let item of items"> <span>{{ item.name }}</span> <span>{{ item.price | currency }}</span> </div> سادسًا، انقر على زر الدفع Checkout لرؤية السلة المحدَّثة، وتذكَّر أنَّ أي تعديلات على التطبيق ستؤدي إلى تحديث المعاينة، وبالتالي إفراغ السلة. انقر على الرابط للانتقال إلى أسعار الشحن. أصبح تطبيقنا الآن يعرض قائمة بالمنتجات وفيه سلة تسوق مع توفير إمكانية عرض الأسعار، وسنعمل في الخطوة التالية على إضافة ميزة الدفع عن طريق تعبئة استمارة تجمع المعلومات المتعلقة بالمستخدم والتي تعد جزءًا من عملية التسوق من المتجر. تعريف نموذج استمارة الدفع توضِّح لك هذه الخطوة كيفية إعداد نموذج استمارة الدفع في صنف المكون حيث يحدّد هذا النموذج حالة الاستمارة. افتح الملف cart.component.ts واستورد خدمة FormBuilder من حزمة ‎@angular/forms، حيث توفِّر هذه الخدمة توابعًا ملائمةً لتوليد عناصر التحكم. الملف src/app/cart/cart.component.ts: import { Component } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { CartService } from '../cart.service'; احقن خدمة FormBuilder في الباني constructor()‎ الخاص بالمكون CartComponent، حيث أنَّ هذه الخدمة هي جزء من وحدة ReactiveFormsModule التي استوردتها للتو. الملف src/app/cart/cart.component.ts: export class CartComponent { constructor( private cartService: CartService, private formBuilder: FormBuilder, ) {} } استخدم تابع group()‎ من FormBuilder من أجل الحصول على اسم المستخدِم وعنوانه وذلك من أجل ضبط خاصية checkoutForm إلى نموذج استمارة يحوي حقلَي name وaddress. الملف src/app/cart/cart.component.ts: export class CartComponent { items = this.cartService.getItems(); checkoutForm = this.formBuilder.group({ name: '', address: '' }); constructor( private cartService: CartService, private formBuilder: FormBuilder, ) {} } عرِّف التابع onSubmit()‎ المسؤول عن معالجة الاستمارة، حيث يسمح هذا التابع للمستخدمين بإرسال أسمائهم وعناوينهم، كما أنَّ هذا التابع يستخدِم تابع clearCart()‎ الخاص بخدمة CartService لإعادة ضبط الاستمارة وتفريغ السلة، ويكون صنف المكوِّن الخاص بالسلة كاملًا كما يلي: الملف src/app/cart/cart.component.ts: import { Component } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { CartService } from '../cart.service'; @Component({ selector: 'app-cart', templateUrl: './cart.component.html', styleUrls: ['./cart.component.css'] }) export class CartComponent { items = this.cartService.getItems(); checkoutForm = this.formBuilder.group({ name: '', address: '' }); constructor( private cartService: CartService, private formBuilder: FormBuilder, ) {} onSubmit(): void { // Process checkout data here this.items = this.cartService.clearCart(); console.warn('Your order has been submitted', this.checkoutForm.value); this.checkoutForm.reset(); } } إنشاء استمارة الدفع اتبع الخطوات التالية لإضافة استمارة الدفع في أسفل قسم العرض الخاص بسلة التسوق. أولًا، أضف وسم <form> وزر الشراء Purchase في نهاية ملف cart.component.html. ثانيًا، استخدِم تربيط الخاصيات لإضافة الخاصية formGroup إلى وسم <form> وربط checkoutForm بها. الملف src/app/cart/cart.component.html: <form [formGroup]="checkoutForm"> <button class="button" type="submit">Purchase</button> </form> ثالثًا، استخدم التربيط عن طريق الحدث ngSubmit ضمن وسم form للاستماع إلى عملية إرسال النموذج واستدعاء تابع onSubmit()‎ مع قيمة checkoutForm. الملف src/app/cart/cart.component.html (تفاصيل قالب مكون السلة): <form [formGroup]="checkoutForm" (ngSubmit)="onSubmit()"> </form> رابعًا، أضف حقول <input> لكل من الاسم name والعنوان address، وأضف لكل منها سمة formControlName التي تربط عناصر تحكم name وaddress الموجودة في استمارة checkoutForm بحقول <input> الخاصة بهم، ويكون المكوِّن كاملًا كما يلي: الملف src/app/cart/cart.component.html: <h3>Cart</h3> <p> <a routerLink="/shipping">Shipping Prices</a> </p> <div class="cart-item" *ngFor="let item of items"> <span>{{ item.name }} </span> <span>{{ item.price | currency }}</span> </div> <form [formGroup]="checkoutForm" (ngSubmit)="onSubmit()"> <div> <label for="name"> Name </label> <input id="name" type="text" formControlName="name"> </div> <div> <label for="address"> Address </label> <input id="address" type="text" formControlName="address"> </div> <button class="button" type="submit">Purchase</button> </form> يستطيع المستخدِمين بعد وضع عناصر قليلة في السلة معاينة تلك العناصر ومن ثم إدخال أسمائهم وعناوينهم وتأكيد عملية الشراء. افتح الطرفية لتأكيد عملية الإرسال، حيث ستجد كائنًا يحتوي على الاسم والعنوان اللذَين أرسلتهما. الخطوة التالية ضبطت في هذا المثال التطبيق الخاص بك حتى تتمكن من عرض تفاصيل كل منتج في عنوان متميز، ولديك الآن تطبيق متجر يحتوي على قائمة بالمنتجات وسلة تسوق ونموذج دفع، وبإمكانك إلقاء نظرة على أسعار الشحن أيضًا. أكمل إلى قسم النشر لتنتقل إلى التطوير المحلي أو تنشر التطبيق الخاص بك إلى Firebase أو الخادم الخاص بك وذلك لمتابعة استكشاف Angular. ترجمة -وبتصرف- للمقال Adding navigation والمقال Managing data والمقال Using forms for user input من موقع angular.io الرسمي. اقرأ أيضًا المتحكمات (Controllers) في AngularJS تعلم البرمجة
  5. يقدِّم لك هذا المقال التعليمي الأساسيات الموجودة في Angular من خلال السير معك في عملية بناء موقع تجارة إلكترونية مؤلف من قائمة من المنتجات، وسلة التسوق، ونموذج الدفع، كما يستخدِم هذا المقال تطبيقًا جاهزًا يمكنك معاينته وتعديله بصورة تفاعلية عن طريق StackBlitz دون الحاجة إلى إعداد بيئة عمل محلية، حيث تُعَدّ StackBlitz بيئة تطوير تعتمد على المتصفح وتمكِّنك من إنشاء وحفظ ومشاركة المشاريع باستخدام مجموعة متنوعة من التقنيات. هذه المقال جزءٌ من سلسلة مقالات حول بناء تطبيق تجارة إلكترونية عبر Angular: ما هي Angular؟ كيفية استعمال Angular في بناء تطبيقات الويب إضافة التنقل وإدارة البيانات في تطبيق Angular تهيئة بيئة تطبيقات Angular ونشرها على الويب المتطلبات الأساسية يجب أن يكون لديك فهم أساسي لما يلي لتحقيق أعلى إفادة من هذا المقال التعليمي: HTML JavaScript TypeScript تفاصيل التطبيق الذي سنبنيه تُبنَى تطبيقات Angular بالاعتماد على المكوِّنات، إذ تمثِِّل هذه المكوِّنات اللبنات الأساسية لبناء واجهة المستخدِم والتي تتيح لك إعادة استخدام مجموعات كبيرة من وظائف واجهة المستخدِم، ويتألف المكوِّن من ثلاثة أشياء: صنف المكوِّن الذي يعالِج البيانات والوظائف. قالب HTML الذي يحدِّد واجهة المستخدِم. التنسيقات الخاصة بالمكوِّن التي تحدِّد الشكل والمظهر. يوضح هذا الدليل كيفية بناء تطبيق باستخدام المكوِّنات التالية: <app-root>: المكوِّن الجذر الذي يُحمَّل أولًا والذي يمثِّل الحاوية للمكوِّنات الأخرى. <app-top-bar>: اسم المتجر وزر الدفع. <app-product-list>: قائمة المنتجات. <app-product-alerts> - مكوِّن يحتوي على تنبيهات التطبيق. البدء مع الملفات الأساسية للمشروع ولِّد مشروع العينة الجاهزة في StackBlitz لإنشاء الملفات الأساسية التي سنتعامل معها، واتبع ما يلي لحفظ عملك: سجّل الدخول إلى StackBlitz. انسخ المشروع الذي ولَّدته. احفظ المشروع بصورة دورية. يعرض جزء المعاينة الموجود على اليمين في StackBlitz حالة بدء تطبيق المثال، كما تعرض المعاينة منطقتين هما: My Store شريط علوي يحوي اسم المتجر وزر الدفع. Products ترويسة من أجل قائمة المنتجات. يعرض جزء المشروع الموجود على اليسار الملفات المصدرية التي يتكون منها التطبيق، كما تتضمن ملفات البنية التحتية وملفات الضبط، في حين ينشئ StackBlitz ملفات أساسية وبيانات وهمية لك عندما تولِّد تطبيقات المثال في StackBlitz والتي تأتي مترافقة مع المقالات التعليمية، كما توجد جميع الملفات التي ستستخدمها خلال المقال التعليمي ضمن مجلد src، ويمكنك الاطلاع على التوثيق الخاص بـ StackBlitz لمزيد من المعلومات حول استخدام StackBlitz. إنشاء قائمة المنتجات سنعمل في هذا القسم على تحديث التطبيق لعرض قائمة المنتجات، حيث سنستخدم بيانات المنتج المعرَّفة مسبقًا في ملف products.ts والتوابع الموجودة في ملف product-list.component.ts، كما سيرشدك هذا القسم خلال عملية تحرير ملف HTML والمعروف باسم القالب أيضًا. بدايةً، افتح ملف القالب product-list.component.html في مجلد product-list وأضف موجِّهًا هيكليًا ngFor للعنصر <div>، كما يلي: الملف src/app/product-list/product-list.component.html: <h2>Products</h2> <div *ngFor="let product of products"> </div> سيتكرر العنصر <div> من أجل كل منتج product في قائمة المنتجات products باستخدام ngFor*، كما تشكّل الموجّهات الهيكلية Structural directives، أو تعيد تشكيل بنية DOM عن طريق إضافة العناصر وإزالتها ومعالجتها. بعد ذلك أضف داخل العنصر <div> العنوان <h3> وضع فيه {{ product.name }} الذي يمثل اسم المنتج وهو مثال عن سياق الإدراج في Angular، حيث يتيح لك الإدراج {{ }} تصيير قيمة الخاصية على أساس نص. الملف src/app/product-list/product-list.component.html: <h2>Products</h2> <div *ngFor="let product of products"> <h3> {{ product.name }} </h3> </div> يُحدَّث جزء المعاينة لعرض اسم كل منتج في القائمة. أضف العنصر <a> حول {{ product.name }} لربط اسم كل منتج بتفاصيله ثم اضبط العنوان ليصبح اسم المنتج باستخدام صياغة التربيط بين الخاصيات Property binding [] كما يلي: الملف src/app/product-list/product-list.component.html: <h2>Products</h2> <div *ngFor="let product of products"> <h3> <a [title]="product.name + ' details'"> {{ product.name }} </a> </h3> </div> مرّر مؤشر الفأرة فوق اسم منتج ما في جزء المعاينة لترى قيمة خاصية التربيط بالاسم، والتي تمثّل اسم المنتج بالإضافة إلى كلمة "تفاصيل". يتيح لك التربيط عن طريق الصياغة [] استخدام قيمة خاصية مُعرَّفة أو موجودة في القالب. أضف توصيفات المنتجات عن طريق استخدام الموجِّه ‎*ngIf على عنصر <p>، حيث ينشئ Angular عنصر <p> فقط في حال كان المنتج الحالي يملك وصفًا. الملف src/app/product-list/product-list.component.html: <h2>Products</h2> <div *ngFor="let product of products"> <h3> <a [title]="product.name + ' details'"> {{ product.name }} </a> </h3> <p *ngIf="product.description"> Description: {{ product.description }} </p> </div> يعرض التطبيق الآن اسم ووصف كل منتج في القائمة، ولاحظ أنّ المنتج الأخير لا يملك فقرة وصف أي لا يملك قيمةً للخاصية description وبالتالي لم تنشئ Angular عنصر <p> له. أخيرًا أضف زرًا حتى يتمكّن المستخدِمون من مشاركة المنتج واربط الحدث click مع التابع ()share في ملف product-list.component.ts، ويكون التربيط بين الأحداث بوضع اسم الحدث بين قوسين () حول الحدث كما هو الحال في حدث (click) ضمن عنصر <button>. الملف src/app/product-list/product-list.component.html: <h2>Products</h2> <div *ngFor="let product of products"> <h3> <a [title]="product.name + ' details'"> {{ product.name }} </a> </h3> <p *ngIf="product.description"> Description: {{ product.description }} </p> <button (click)="share()"> Share </button> </div> يملك كل منتج الآن زر مشاركة Share. تطلق عملية النقر على زر المشاركة تنبيهًا يخبرنا بأنه "شورِك المنتج!". بهذا تكون قد استكشفت الآن الميزات الشائعة الاستخدام أثناء العمل في قوالب Angular. تمرير البيانات إلى مكون ابن تعرض حاليًا قائمة المنتجات اسم وتوصيف كل منتج، كما يعرِّف ProductListComponent أيضًا خاصية products التي تحتوي على بيانات مستورَدة لكل منتج من مصفوفة products في products.ts، وتكون الخطوة التالية هي إنشاء ميزة تنبيه جديدة تستخدِم بيانات المنتج من ProductListComponent. يتحقّق التنبيه من سعر المنتج، فإذا كان السعر أكبر من 700 دولار، سيعرض زر نبهني Notify Me الذي يتيح للمستخدِمين الاشتراك في الإشعارات عندما يُطرَح المنتج للبيع، كما سيرشدك هذا القسم خلال عملية إنشاء مكوِّن ابن ProductAlertsComponent يمكنه تلقي البيانات من المكوِّن الأب ProductListComponent. بدايةً، انقر بزر الفأرة الأيمن على مجلد app واستخدم المولد الخاص بـ Angular لإنشاء مكون جديد يُسمّى product-alerts. ينشئ المولِّد ملفات البدء الأساسية لأجزاء المكوِّن الثلاثة: product-alerts.component.ts product-alerts.component.html product-alerts.component.css افتح ملف product-alerts.component.ts وستجد فيه مزخرفًا decorator باسم ‎@Component()‎ يشير إلى أن الصنف الحالي هو مكوِّن، كما يوفِّر بيانات وصفية حوله بما في ذلك المحدِّد selector والقوالب templates والتنسيقات styles. الملف src/app/product-alerts/product-alerts.component.ts: import { Component, OnInit } from '@angular/core'; @Component ({ selector: 'app-product-alerts', templateUrl: './product-alerts.component.html', styleUrls: ['./product-alerts.component.css'] }) export class ProductAlertsComponent implements OnInit { constructor() { } ngOnInit() { } } تتمثَّل الميزات الرئيسية في ‎@Component()‎ بما يلي: يعرّف المحدد selector اسم المكون وقيمته هنا app-product-alerts، كما تعارف مبرمجو Angular على بدء اسم المكون بالبادئة -app متبوعة باسم المكوِّن. تشير أسماء ملفات القوالب والتنسيقات إلى HTML وCSS للمكوِّن. يصدِّر تعريف ‎@Component()‎ الصنف ProductAlertsComponent أيضًا والذي يعالِج وظائف المكوِّن. ستورد Input من ‎@angular/core أولًا لإعداد مكوِّن ProductAlertsComponent لاستقبال بيانات المنتج. الملف src/app/product-alerts/product-alerts.component.ts: import { Component, OnInit } from '@angular/core'; import { Input } from '@angular/core'; import { Product } from '../products'; أضف في تعريف الصنف ProductAlertsComponent خاصيةً تسمى product مع مزخرف ‎@Input()‎ والذي يشير إلى تمرير قيمة الخاصية إلى المكوِّن الرئيسي ProductListComponent. الملف src/app/product-alerts/product-alerts.component.ts: export class ProductAlertsComponent implements OnInit { @Input() product!: Product; constructor() { } ngOnInit() { } } افتح الملف product-alerts.component.html واستبدل فقرة العنصر النائب بزر نبّهني Notify Me الذي يظهر في حال كان سعر المنتج أكثر من 700 دولار. الملف src/app/product-alerts/product-alerts.component.html: <p *ngIf="product && product.price > 700"> <button>Notify Me</button> </p> لاحظ أن المولد يضيف تلقائيًا الصنف ProductAlertsComponent إلى الوحدة AppModule ليكون متاحًا في كافة مكونات التطبيق: import { ProductAlertsComponent } from './product-alerts/product-alerts.component'; @NgModule({ declarations: [ AppComponent, TopBarComponent, ProductListComponent, ProductAlertsComponent, ], أضف المحدد <app-product-alerts> إلى product-list.component.html لعرض ProductAlertsComponent على أساس ابن للمكوِّن ProductListComponent، ومرِّر المنتج الحالي على أساس دخل إلى المكوِّن باستخدام التربيط عن طريق تربيط الخاصية property binding. الملف src/app/product-list/product-list.component.html: <button (click)="share()"> Share </button> <app-product-alerts [product]="product"> </app-product-alerts> يأخذ مكوِّن التنبيه الجديد الخاص بالمنتج منتجًا من قائمة المنتج على أساس دخل له، حيث يعرض أو يخفي عن طريقه زر نبهني Notify Me بناءً على قيمة سعر هذا المنتج، فسعر هاتف Phone XL مثلًا، هو أكثر من 700 دولار لذلك عُرِض زر نبهني Notify Me لهذا المنتج. تمرير البيانات إلى المكون الرئيسي يحتاج المكون الابن إلى التنبيه وإرسال البيانات إلى المكوِّن الأب لكي يعمل زر نبهني Notify Me، كما يحتاج ProductAlertsComponent إلى إطلاق حدث عندما ينقر المستخدِم على نبهني Notify Me، في حين يحتاج ProductListComponent إلى الاستجابة للحدث. أولًا، استورد Output و EventEmitter في الملف product-alerts.component.ts من ‎@angular/core. الملف src/app/product-alerts/product-alerts.component.ts: import { Component, Input, Output, EventEmitter } from '@angular/core'; import { Product } from '../products'; ثانيًا، عرِّف خاصيةً تسمى notify في صنف المكوِّن مع مزخرف ‎@Output()‎ ونسخة من EventEmitter()‎، هيِّئ المكوِّن ProductAlertsComponent باستعمال ‎@Output()‎ لكي تسمح لمكوِّن ProductAlertsComponent بإطلاق حدث عندما تتغير قيمة الخاصية notify. الملف src/app/product-alerts/product-alerts.component.ts: export class ProductAlertsComponent { @Input() product: Product|undefined; @Output() notify = new EventEmitter(); } ثالثًا، عدِّل زر نبهني Notify Me ضمن product-alerts.component.html باستخدام التربيط مع الحدث من أجل استدعاء الدالة notify.emit()‎. الملف src/app/product-alerts/product-alerts.component.html: <p *ngIf="product && product.price > 700"> <button (click)="notify.emit()">Notify Me</button> </p> رابعًا، عرِّف سلوكًا يحدث عندما ينقر المستخدِم على الزر، حيث يبدأ المكون الأب ProductListComponent وليس المكون ProductAlertsComponent بالعمل عندما يولِّد الابن الحدث. عرّف تابع onNotify()‎ ضمن product-list.component.ts بطريقة مشابهة للتابعshare()‎. الملف src/app/product-list/product-list.component.ts: export class ProductListComponent { products = products; share() { window.alert('The product has been shared!'); } onNotify() { window.alert('You will be notified when the product goes on sale'); } } خامسًا، حدِّث ProductListComponent ليستقبل البيانات من ProductAlertsComponent، واربط <app-product-alerts> مع التابع onNotify()‎ الموجود في مكون قائمة المنتجات ضمن الملف product-list.component.html، حيث ما يعرضه الزر نبهني Notify Me هو <app-product-alerts>. الملف src/app/product-list/product-list.component.html: <button (click)="share()"> Share </button> <app-product-alerts [product]="product" (notify)="onNotify()"> </app-product-alerts> سادسًا، انقر على زر نبهني Notify Me لإطلاق التنبيه والذي يطبع رسالة "سوف تُعلَم عندما يُطرح المنتج للبيع" على المتصفح. الخطوة التالية أنشأتَ في هذا المقال تطبيقًا يمرّ على مجموعة من البيانات، كما أنشأتَ مكوِّنات مميزةً تتواصل مع بعضها، وسنكمل في المقال التالي استشكاف Angular وإنشاء التطبيق الخاص بك عبر مقال إضافة التنقل وإدارة البيانات في تطبيق Angular لإنشاء صفحة تفاصيل المنتج وقائمة المنتجات قبل أن ننتقل إلى خطوة نشر التطبيق على خادم الويب أو أي خادم تريد. ترجمة -وبتصرف- للمقال Getting started with Angular من موقع angular.io الرسمي. اقرأ أيضًا أساسيات بناء تطبيقات الويب الخطوات الأولى في بناء تطبيقات الويب باستعمال TypeScript مبادئ AngularJS تعلم البرمجة
  6. سيساعدك هذا المقال على فهم إطار العمل Angular -والذي ينطق أنجولار- كما سيجاوب على الأسئلة التالية: ما هي منصة Angular؟ وما هي الميزات التي تقدمها؟ وما الذي تأمل أن تبنيه عندما تبدأ بناء تطبيقاتك باستخدام Angular؟ تُعَدّ Angular منصة تطوير مبنية على لغة TypeScript وتتضمن ما يلي: إطار عمل قائم على المكوِّنات من أجل بناء تطبيقات ويب قابلة للتوسع. مجموعةً من المكتبات المضمنة التي تقدِّم مجموعةً واسعةً من الميزات بما فيها التوجيه وإدارة النماذج واتصالات الخادم والعميل والكثير من الميزات الأخرى. مجموعةً من أدوات المطوِّرين التي تساعدك في عملية تطوير شيفرة المشروع وبنائها واختبارها وتحديثها. تقدِّم منصة Angular فوائدًا عديدةً لمستخدميها، حيث ستعمل على توسيع المشاريع التي يعمل عليها مطور واحد إلى مشاريع على مستوى المؤسسة، حيث صُمِّمت Angular لجعل عملية التحديث سهلةً قدر الإمكان، وبالتالي تحقيق إفادة من أحدث التطورات بأقل جهد ممكن، والأفضل من ذلك أنه تتألف المنظومة الخاصة بـ Angular من مجموعة متنوعة تضم أكثر من 1.7 مليون مطوِّر ومؤلف مكتبات ومنشئ محتوى. تطبيقات Angular: الأساسيات يشرح هذا القسم الفكرة الأساسية وراء إنشاء Angular، إذ سيساعدك فهم الأفكار الواردة في هذا القسم في تصميم وبناء تطبيقاتك بصورة فعالة أكثر. المكونات تُعَدّ المكوِّنات أحجار البناء الأساسية التي تكوِّن التطبيق، فيحتوي المكوِّن على صنف TypeScript مع مزخرِف ‎@Component()‎ وقالب HTML وملف التنسيقات، حيث يعرِّف مزخرِف ‎@Component()‎ المعلومات التالية في Angular: محدِّد CSS الذي يعرِّف الطريقة التي سيُستخدَم بها المكوِّن ضمن قالب ما، حيث تصبح عناصر HTML التي تستخدمها ضمن القالب الخاص بك والتي تطابق هذا المحدِّد نسخةً من هذا المكوِّن. قالب HTML الذي يوجّه Angular إلى كيفية إخراج هذا المكوِّن. مجموعة اختيارية من تنسيقات CSS التي تعرِّف المظهر الخاص بعناصر قالب HTML. يُعَدّ الكود التالي أبسط صورة لمكوِّن Angular: import { Component } from '@angular/core'; @Component({ selector: 'hello-world', template: ` <h2>Hello World</h2> <p>This is my first component!</p> ` }) export class HelloWorldComponent { // تقود الشيفرة الموجودة في هذا الصنف السلوك الخاص بالمكوِّن } يجب كتابة الشيفرة التالية ضمن قالب HTML الخاص بك من أجل استخدام هذا المكوِّن: <hello-world></hello-world> سيكون ناتج شجرة DOM عندما يقوم Angular بإخراج هذا المكوِّن وتصييره على الصورة التالية: <hello-world> <h2>Hello World</h2> <p>This is my first component!</p> </hello-world> يوفِّر نموذج مكوِّنات Angular تغليفًا متينًا وبنية تطبيق سهلة الاستخدام، كما تجعل المكوِّنات تطبيقك مريحًا جدًا عند إجراء اختبار الوحدة وتحسِّن من قابلية القراءة للشيفرة الخاصة بك. القوالب يملك كل مكوِّن قالب HTML يحدِّد كيفية إخراج هذا المكوِّن وعرضه، كما يمكنك تعريف هذا القالب عن طريق كتابة الشيفرات بصورة خطية أو عن طريق ملف خارجي يُحدَّد مساره، في حين يوسِّع Angular لغة HTML من خلال بنى لغوية إضافية والتي تمكنك من إضافة قيم متغيرة من خلال المكوِّن الخاص بك، حيث تقوم Angular تلقائيًا بتحديث شجرة DOM المصيَّرة الناتجة عندما تتغير حالة المكوِّن الخاص بك، وتتمثل إحدى التطبيقات على هذه الميزة في إدراج نص آلي كما في المثال التالي: <p>{{ message }}</p> تأتي القيمة المرتبطة بالرسالة عن طريق صنف المكوِّن: import { Component } from '@angular/core'; @Component ({ selector: 'hello-world-interpolation', templateUrl: './hello-world-interpolation.component.html' }) export class HelloWorldInterpolationComponent { message = 'Hello, World!'; } تكون النتيجة التي يراها المستخدِم عندما يحمِّل التطبيق المكوِّن وقالبه كما يلي: <p>Hello, World!</p> يُخبر استخدام الأقواس المعقوصة بأنه يجب على Angular استبدال القيمة الموجودة ضمنها بالقيمة المقابلة لها والموجودة ضمن صنف المكوِّن، كما يدعم Angular عملية التربيط بين الخاصيات property bindings من أجل مساعدتك على ضبط القيم لكل من الخصائص والسمات لعناصر HTML وتمرير هذه القيم إلى قسم العرض الخاص بتطبيقك. <p [id]="sayHelloId" [style.color]="fontColor">You can set my color in the component!</p> تشير الأقواس المعقوفة هنا إلى عملية ربط الخاصية أو السمة بقيمة موجودة ضمن صنف المكوِّن، كما يمكنك في Angular أيضًا تعريف مستمعي الحدث والتي مهمتها الاستماع إلى الأحداث الخاصة بالمستخدِم والاستجابة لها مثل حدث الضغط على زر ما وحركات الفأرة والنقرات وحتى اللمسات، حيث يمكنك تعريف مستمع الحدث من خلال تحديد اسم الحدث ضمن أقواس: <button (click)="sayMessage()" [disabled]="canClick">Trigger alert message</button> يستدعي المثال السابق تابعًا والذي عُرِّف ضمن صنف المكوِّن: sayMessage() { alert(this.message); } يُعَدّ المثال التالي مثالًا شاملًا لتوضيح كل من عمليات التبديل والتربيط بين الخاصيات والتربيط مع الحدث ضمن قالب Angular: الملف hello-world-bindings.component.ts: import { Component } from '@angular/core'; @Component ({ selector: 'hello-world-bindings', templateUrl: './hello-world-bindings.component.html' }) export class HelloWorldBindingsComponent { fontColor = 'blue'; sayHelloId = 1; canClick = false; message = 'Hello, World'; sayMessage() { alert(this.message); } } الملف hello-world-bindings.component.html: <button [disabled]="canClick" (click)="sayMessage()"> Trigger alert message </button> <p [id]="sayHelloId" [style.color]="fontColor"> You can set my color in the component! </p> <p>My color is {{ fontColor }}</p> أضف وظائف أخرى إلى القوالب الخاصة بك من خلال استخدام الموجِّهات directives، وأكثر الموجِّهات شهرةً في Angular هي الموجه ‎*ngIf والموجه ‎*ngFor، حيث تستطيع الموجّهات أداء العديد من المهام المتنوعة مثل تعديل البنية الخاصة بشجرة DOM بصورة آلية وإنشاء الموجِّه الخاص بك من أجل خلق تجربة مستخدِم جيدة، وتُعَدّ الشيفرة التالية مثالًا عن استخدام الموجِّه ngIf*: الملف hello-world-ngif.component.ts: import { Component } from '@angular/core'; @Component({ selector: 'hello-world-ngif', templateUrl: './hello-world-ngif.component.html' }) export class HelloWorldNgIfComponent { message = 'I\'m read only!'; canEdit = false; onEditClick() { this.canEdit = !this.canEdit; if (this.canEdit) { this.message = 'You can edit me!'; } else { this.message = 'I\'m read only!'; } } } الملف hello-world-ngif.component.html <h2>Hello World: ngIf!</h2> <button (click)="onEditClick()">Make text editable!</button> <div *ngIf="canEdit; else noEdit"> <p>You can edit the following paragraph.</p> </div> <ng-template #noEdit> <p>The following paragraph is read only. Try clicking the button!</p> </ng-template> <p [contentEditable]="canEdit">{{ message }}</p> تسمح لك القوالب التصريحية declarative templates في Angular بفصل منطق التطبيق الخاص بك عن طريقة العرض، كما تستند القوالب إلى لغة HTML القياسية لسهولة البناء والصيانة والتحديث. حقن التبعية يتيح لك حقن التبعية Dependency injection تعريف التبعيات الخاصة بأصناف TypeScript الخاصة بك دون الاهتمام باستنساخها، إذ تقوم Angular بعملية الاستنساخ عوضًا عن ذلك، كما يوفِّر لك نمط التصميم بهذه الطريقة إمكانية كتابة شيفرة مرنة وسهلة الاختبار، فعلى الرغم من أن فهم مصطلح حقن التبعية ليس بهذا الأمر الجلل للبدء باستخدام Angular، إلا أننا نوصي به من أجل ممارسة أفضل، كما تستفيد العديد من الجوانب في Angular من ميزاته إلى حد ما، وإليك المثال التالي لتوضيح عملية حقن التبعية، حيث يعرِّف الملف الأول logger.service.ts صنف Logger، إذ يحتوي هذا الصنف على دالة writeCount والتي تطبع عددًا ضمن الطرفية: import { Injectable } from '@angular/core'; @Injectable({providedIn: 'root'}) export class Logger { writeCount(count: number) { console.warn(count); } } تاليًا، يُعرِّف ملف hello- world- di.component.ts مكوِّن Angular، حيث يحتوي هذا المكوِّن على زر يستخدِم دالة writeCount من صنف Logger، كما حُقِنت خدمة Logger في صنف HelloWorldDI من أجل استخدام هذه الدالة من خلال إضافة private logger: Logger إلى الباني. import { Component } from '@angular/core'; import { Logger } from '../logger.service'; @Component({ selector: 'hello-world-di', templateUrl: './hello-world-di.component.html' }) export class HelloWorldDependencyInjectionComponent { count = 0; constructor(private logger: Logger) { } onLogMe() { this.logger.writeCount(this.count); this.count++; } } واجهة سطر أوامر Angular تُعَدّ Angular CLI من أسرع الطرق المباشرة والمحبّذة لبناء تطبيقات Angular، إذ يقلل استخدام Angular CLI من حدوث المشاكل عند تشغيل المهام، وهنا نورد بعض الأمثلة: table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } الأمر شرح الوظيفة ng build يترجِم تطبيق Angular إلى دليل الإخراج. ng serve يبني ويقدِّم التطبيق الخاص بك ويُعيد بناء التطبيق في حال حدوث تغيرات على الملف. ng generate يولِّد ويعدِّل الملفات بناءً على تخطيط مسبق. ng test تشغيل اختبارات الوحدة unit tests على مشروع معطى. ng e2e يبني ويقدِّم تطبيق Angular ثم يشغِّل اختبارات نهاية- إلى- نهاية. ستجد أنّ Angular CLI أداة مفيدة جدًا من أجل بناء تطبيقاتك. المكتبات الداخلية قدَّم قسم "تطبيقات Angular: الأساسيات" في الأعلى لمحةً مختصرةً عن اثنين من العناصر الهرمية الرئيسية التي ستستخدمها عند إنشاء تطبيقات Angular، لكن تصبح الفوائد العديدة لـ Angular أكثر وضوحًا عندما ينمو تطبيقك وتود إضافة وظائف إضافية عليه مثل التنقل في الموقع أو إدخالات المستخدِم، لذا استخدِم منصة Angular لتدمج إحدى مكتبات Angular الداخلية العديدة، وتوجد بعض المكتبات المتاحة لك إضافتها كما يلي: المكتبة الشرح Angular Router عملية تنقّل وتوجيه متقدمة من جانب العميل بالاستناد إلى مكوِّنات Angular، كما تدعم التحميل البطيء والمسارات المتداخلة ومطابقة مسار مخصص وغيرها الكثير. Angular Forms نظام موحَّد للمشاركة في النموذج والتحقق منه. Angular HttpClient عميل HTTP قوي يمكنه إجراء اتصالات أكثر تقدمًا بين الخادم والعميل. Angular Animations نظام غني للتحكم بالحركات في الموقع بالاعتماد على حالة التطبيق. Angular PWA أدوات لبناء تطبيقات الويب التقدمية PWA تتضمن عامل الخدمة service worker وقائمة تطبيق الويب Web app manifest. Angular Schematics شيفرة مساعدة آلية بالإضافة إلى إعادة التوليد وأدوات التحديث التي تبسِّط عملية التطوير على نطاق واسع. تعمل هذه المكتبات على توسيع وظائف التطبيق الخاص بك، كما تسمح لك بالتركيز بصورة أكبر على الميزات التي تجعل تطبيقك فريدًا، لهذا عليك إضافة هذه المكتبات المصممة لتتكامل بسلاسة مع إطار العمل Angular وتحديثه، ويمكنك استخدام هذه المكتبات فقط عندما يمكنها مساعدتك في إضافة وظائف أخرى إلى تطبيقاتك أو في حل مشكلة معيَّنة. الخطوات التالية يهدف هذا المقال إلى إعطائك لمحةً موجزةً عن Angular والمزايا التي يوفِّرها وما الذي تأمل أن تبنيه عندما تبدأ في إنشاء تطبيقاتك، كما يمكنك الانتقال إلى المقال التالي كيفية استعمال Angular في بناء تطبيقات الويب لتشاهد طريقة عمل Angular، حيث تستخدِم هذه المقالة بيئة stackblitz.com التجريبية لتتمكن من تجربة أمثلة عملية من Angular دون تثبيت أي متطلبات. ترجمة -وبتصرف- للمقال What is Angular?‎ من موقع angular.io الرسمي. اقرأ أيضًا مدخل إلى تعلم AngularJS تعلم البرمجة
  7. يمكنك الاطلاع على هذه المقالة لمعرفة حل مشكلتك .. رابط المقالة: https://neosmart.net/wiki/0xc000000e_selected_entry_could_not_be_loaded/ https://answers.microsoft.com/en-us/windows/forum/all/meet-error-0xc000000e-after-installing-an-ssd-on/d5805063-6aad-4451-a6c1-3e208c858df6
  8. نعم تستطيع ذلك إذا كانت لديك معرفة بسيطة بدوال الاكسيل فمن خلال شرط if يمكنك التحقق من القيم الموجودة في الخانات المنفصلة ودمجها في الخانة المطلوب كتابة نوع القطعة فيها
×
×
  • أضف...