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

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

هذا المقال جزء من سلسلة عن بناء مدونة باستخدام إطار العمل Angular وقاعدة بيانات Firestore

حذف تدوينة

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

ng g s services/Snackbar

افتح الملف snackbar.service.ts وضع الشيفرة التالية فيه:

import { Injectable } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';

@Injectable({
providedIn: 'root'
})
export class SnackbarService {

 constructor(private snackBar: MatSnackBar) { }

 showSnackBar(message: string) {
  this.snackBar.open(message, 'Close', {
   duration: 2000,
   panelClass: 'snackbar-ribon',
   verticalPosition: 'top',
   horizontalPosition: 'center'
  });
 }
}

لقد أنشأنا الآن التابع showSnackBar الذي يقبل معامِلًا يكون هو الرسالة التي نريد عرضها، والحد الأقصى لعرض هذه الرسالة هو 2000 مللي ثانية، وقد عرَّفنا موضع الشريط لينتصف أعلى الصفحة.

أضف الآن التنسيق التالي لشريط Snackbar في الملف styles.scss:

.snackbar-ribon {
 color: #FFFFFF;
 background: #17a2b8;
}

سنضيف الآن مزية حذف إحدى التدوينات الموجودة، وسنحذف التدوينة من تجميعة blogs وفقًا لمعرِّف التدوينة postID.

أضف الشيفرة التالية في الملف blog.service.ts:

deletePost(postId: string) {
 return this.db.doc('blogs/' + postId).delete();
}

افتح الملف blog-card.comonent.ts واستورد SnackbarService كما يلي:

deletePost(postId: string) { return this.db.doc('blogs/' + postId).delete();}

ثم احقن خدمة SnackbarService في المنشئ كما يلي:

constructor(
 // other service injection
 private snackBarService: SnackbarService
 ) { }

كما سنحدّث تابع الحذف في الملف blog-card.comonent.ts، بحيث يكون تعريفه كما يلي:

delete(postId: string) {
 if (confirm('Are you sure')) {
  this.blogService.deletePost(postId).then(
   () => {
    this.snackBarService.showSnackBar('Blog post deleted
successfully');
   }
  );
 }
}

سيقبل تابع الحذف postID كمعامِل وسيعرض رسالة تحذير تطلب تأكيد الحذف، وعند ضغط المستخدم على OK، يُستدعَى تابع deletePost الخاص بالخدمة BlogService، ثم نعرض رسالة تظهر نجاح عملية الحذف باستخدام شريط snackbar.

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

delete-confirm.png

إذا ضغط المستخدم على OK ستُحذف التدوينة ونحصل على رسالة توكيد في شريط Snackbar:

deleted-post.png

التعديل على تدوينة موجودة

سننفذ الآن خاصية التعديل على تدوينة قائمة وموجودة، من خلال إضافة التعريف التالي إلى الملف blog.service.ts:

updatePost(postId: string, post: Post) {
 const putData = JSON.parse(JSON.stringify(post));
 return this.db.doc('blogs/' + postId).update(putData);
}

سيقبل التابع updatePost كلا من postID وكائنًا من النوع Post كمعاملات، وسنحلل كائن Post إلى كائن JSON ثم نحدّث الكائن في تجميعة blogs.

أضف توجيه وظيفة التعديل في الملف app.module.ts كما يلي:

RouterModule.forRoot([
 ...
 { path: 'editpost/:id', component: BlogEditorComponent },
 ...
])

ثم أضف تعريف الاستيراد التالي في الملف blog-editor.component.ts:

import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

صرِّح الآن عن متغير Subject جديد:

private unsubscribe$ = new Subject<void>();

افتح الملف blog-editor.component.ts وأضف الشيفرة التالية في المنشئ:

if (this.route.snapshot.params['id']) {
 this.postId = this.route.snapshot.paramMap.get('id');
}

في الشيفرة أعلاه، نستخدم الصنف ActivatedRoute لجلب معرِّف التدوينة من الرابط.

سنضيف الآن تابعًا لإعداد استمارة التعديل عند النقر على زر Edit في بطاقة التدوينة في الصفحة الرئيسية، ويكون تعريف التابع كما يلي:

setPostFormData(postFormData) {
 this.postData.title = postFormData.title;
 this.postData.content = postFormData.content;
}

حدِّث الآن التابع ngOnInit داخل الصنف BlogEditorComponent كما يلي:

ngOnInit() {
 this.setEditorConfig();
 if (this.postId) {
  this.formTitle = 'Edit';
  this.blogService.getPostbyId(this.postId)
   .pipe(takeUntil(this.unsubscribe$))
   .subscribe(
    result => {
     this.setPostFormData(result);
    }
   );
 }
}

إذا كان postID معيَّنًا set فهذا يعني أن هذا طلب تحرير Edit، وسنجعل عنوان الاستمارة Edit، ونستدعي التابع getPostbyID من BlogService لجلب تفاصيل التدوينة المتوافقة مع postID.

عند النقر على Save، نحتاج إلى معالجة كلا من حالة إنشاء تدوينة جديدة وتحرير تدوينة موجودة من قبل، وعليه سنحدِّث saveBlogPost كما يلي:

saveBlogPost() {
if (this.postId) {
 this.blogService.updatePost(this.postId, this.postData).then(
  () => {
   this.router.navigate(['/']);
  }
 );
} else {
  this.postData.createdDate = this.datePipe.transform(Date.now(), 'MMdd-yyyy HH:mm');
  this.blogService.createPost(this.postData).then(
   () => {
    this.router.navigate(['/']);
   }
  );
 }
}

سننفذ الواجهة OnDestroy على الصنف BlogEditorComponent، وسنكمل اشتراك unsubscribe$‎ داخل التابع ngOnDestroy، انظر الشيفرة التالية:

ngOnDestroy() {
 this.unsubscribe$.next();
 this.unsubscribe$.complete();
}

افتح المتصفح وانقر على زر Edit في بطاقة التدوينة في الصفحة الرئيسية، ستنتقل الصفحة إلى صفحة Edit Post. تستطيع الآن أن ترى محرر التدوينة وفيه محتوى التدوينة جاهزًا بداخله، وإذا نظرت إلى الرابط ستجد أنه يحتوي على postID الخاص بهذه التدوينة. انظر الصورة التالية:

editpost.png

يمكن الآن تعديل محتوى التدوينة داخل المحرر، وبعد تمام التعديل ننقر على زر الحفظ لتحديث التدوينة بالمحتوى الجديد والعودة إلى الصفحة الرئيسية لنرى المحتوى الجديد في ملخص التدوينة:

updated-post.png

إضافة شريط تنقل بين صفحات التدوينات

سنضيف خاصية الترقيم pagination في الصفحة الرئيسية وذلك للتنقل بين صفحات بطاقات التدوينات، مستخدمين ngx-pagination لهذا الغرض، وهو مكون مفتوح المصدر يوفر خاصية ترقيم بسيطة وسهلة الاستخدام لتطبيقات Angular.

نفِّذ الأمر التالي لتثبيت المكون ngx-pagination:

npm i ngx-pagination --save

استورد NgxPaginationModule في الملف app.module.ts كما يلي:

import { NgxPaginationModule } from 'ngx-pagination';

@NgModule( {
 imports: [
  ...
  NgxPaginationModule,
 ],
})

إنشاء مكون المرقم PaginatorComponent

نفِّذ الأمر التالي في الطرفية الأصلية لتوليد مكون المرقِّم PaginatorComponent:

ng g c components/paginator

افتح الملف paginator.component.ts وأضف تعريفات الاستيراد التالية:

import { Input } from '@angular/core';
import { Router } from '@angular/router';

سنضيف خاصيتي إدخال لهذا المكون، كما يلي:

@Input()
 pageSizeOptions: []; 

@Input()
 config: any;

احقن صنف الموجِّه router داخل المنشئ، كما يلي:

constructor(private router: Router) { }

سنضيف تابعًا يعالج حدث pageChange للمرقِّم الخاص بنا، وسيكون تعريف ذلك التابع كما يلي:

pageChange(newPage: number) {
 this.router.navigate(['/page/', newPage]);
}

كذلك، نضيف التابع changePageItemCount في الصنف PaginatorComponent، ويُستخدم هذا التابع لإعداد ترقيم ديناميكي للمرقِّم، حيث يعين عدد العناصر التي يظهرها على كل صفحة وفقًا لاختيار من قائمة منسدلة:

changePageItemCount(selectedItem) {
 localStorage.setItem('pageSize', selectedItem.value);
 this.config.itemsPerPage = selectedItem.value;
}

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

افتح الملف paginator.component.html واستبدل الشيفرة التالية بالموجود فيه:

<div class="paginator-controls">
 <div>
  <pagination-controls (pageChange)="pageChange($event)" class="mypagination"></pagination-controls>
 </div>
 <div>
  <mat-form-field>
  <mat-label>Items per page: </mat-label>
  <mat-select [(ngModel)]="config.itemsPerPage"
(selectionChange)="changePageItemCount($event)">
   <mat-option *ngFor="let page of pageSizeOptions" [value]="page">
    {{ page }}
   </mat-option>
  </mat-select>
  </mat-form-field>
 </div>
</div>

أخيرًا، سنضيف التنسيق الخاص بالمكون PaginatorComponent، فافتح الملف paginator.component.scss واستبدل الشيفرة أدناه بالموجود فيه:

.my-pagination ::ng-deep .ngx-pagination {
 margin: 10px 0px 10px 0px;
 padding-inline-start: 0px;
}
.paginator-controls {
 display: flex;
 justify-content: space-between;
 padding-top: 10px;
}
@media screen and (min-width: 320px) and (max-width: 420px) {
 .paginator-controls {
  flex-direction: column-reverse;
 }
}

أما الآن، فسنضيف رابط موجِّهٍ في الملف app.module.ts لدعم الترقيم كما يلي:

 { path: 'page/:pagenum', component: HomeComponent },

إضافة المكون PaginatorComponent إلى قائمة التدوينات

يجب أن نضيف المكون PaginatorComponent إلى مكون قائمة التدويناتBlogCardComponent من أجل تفعيل الترقيم فيها، لنصرح عن خاصيتين، كما يلي:

config: any;
pageSizeOptions = [];

أضف الاستيراد للصنف ActivatedRoute في المكون، كما يلي:

import { ActivatedRoute } from '@angular/router';

احقن الصنف ActivatedRoute في المنشئ، كما يلي:

constructor(
 // other services
 private route: ActivatedRoute) { }

أضف الشيفرة التالية داخل منشئ الصنف BlogCardComponent لبدء الخصائص المصرَّح عنها للتو:

this.pageSizeOptions = [2, 4, 6];
const pageSize = localStorage.getItem('pageSize');
this.config = {
 currentPage: 1,
 itemsPerPage: pageSize ? +pageSize : this.pageSizeOptions[0]
};

والآن، حدِّث التابع ngOnInit كما يلي:

ngOnInit() {
 this.route.params.subscribe(
  params => {
   this.config.currentPage = +params['pagenum'];
   this.getBlogPosts();
  }
 );
}

تحديث قالب BlogCardComponent

افتح الملف blog-card.component.html وأضف مكون المرقِّم كما يلي:

<app-paginator [pageSizeOptions]="pageSizeOptions"
[config]="config"></app-paginator>

سنضيف أنبوب ترقيم أيضًا في التوجيه ngFor أثناء التكرار على قائمة التدوينات، كما يلي:

<div *ngFor="let post of blogPost | paginate: config">

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

chkpt6.png

خاتمة

تعلمنا في هذا الجزء من سلسلة بناء مدونة باستخدام إطار العمل Angular وقاعدة بيانات Firestore كيفية حذف التدوينات والتعديل عليها وإضافة الترقيم للصفحات، وسنتعلم في المقال التالي كيفية إضافة استيثاق جوجل للوصول إلى التطبيق وإضافة التدوينات والتعديل عليها.

ترجمة -وبتصرف- لفصول من كتاب Build a full stack web application using angular and firebase لصاحبه Ankit Sharma.

اقرأ أيضًا


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

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

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



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

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

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

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

  Only 75 emoji are allowed.

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

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

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


×
×
  • أضف...