1. مقدمه
آخرین به روز رسانی: 2020-09-11
چیزی که خواهی ساخت
در این کد لبه، ما یک برد وب کانبان با Angular و Firebase می سازیم! برنامه نهایی ما دارای سه دسته کار است: عقب ماندگی، در حال انجام و تکمیل شده. ما قادر خواهیم بود با کشیدن و رها کردن، کارها را ایجاد، حذف و از یک دسته به دسته دیگر منتقل کنیم.
ما رابط کاربری را با استفاده از Angular توسعه خواهیم داد و از Firestore به عنوان فروشگاه دائمی خود استفاده خواهیم کرد. در انتهای کد لبه، برنامه را با استفاده از Angular CLI در Firebase Hosting مستقر خواهیم کرد.

چیزی که یاد خواهید گرفت
- نحوه استفاده از متریال Angular و CDK.
- چگونه یکپارچه سازی Firebase را به برنامه Angular خود اضافه کنید.
- چگونه داده های دائمی خود را در Firestore نگه دارید.
- چگونه برنامه خود را با استفاده از Angular CLI با یک فرمان در میزبانی Firebase مستقر کنید.
آنچه شما نیاز دارید
این کد لبه فرض می کند که شما یک حساب Google دارید و درک اولیه ای از Angular و Angular CLI دارید.
بیایید شروع کنیم!
2. ایجاد یک پروژه جدید
ابتدا اجازه دهید یک فضای کاری 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 برگشتید، باید یک صفحه خالی ببینید.
3. افزودن مواد و CDK
Angular با پیاده سازی اجزای رابط کاربری سازگار با Material Design به عنوان بخشی از بسته @angular/material عرضه می شود. یکی از وابستگی های @angular/material ، Component Development Kit یا CDK است. CDK ابزارهای اولیه مانند ابزارهای a11y، کشیدن و رها کردن، و پوشش را فراهم می کند. ما CDK را در بسته @angular/cdk توزیع می کنیم.
برای افزودن مطالب به برنامه خود اجرا کنید:
ng add @angular/material
اگر میخواهید از سبکهای تایپوگرافی مواد جهانی استفاده کنید، و اگر میخواهید انیمیشنهای مرورگر را برای Angular Material تنظیم کنید، این دستور از شما میخواهد که یک موضوع را انتخاب کنید. "نیلی/صورتی" را انتخاب کنید تا به همان نتیجه ای که در این کد لبه وجود دارد، برسید و به دو سوال آخر با "بله" پاسخ دهید.
دستور 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>
در اینجا، یک نوار ابزار را با استفاده از رنگ اصلی طرح زمینه متریال خود اضافه می کنیم و در داخل آن از نماد 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 { }
از آنجایی که ما از نوار ابزار و نماد Angular Material استفاده می کنیم، باید ماژول های مربوطه را در AppModule وارد کنیم.
اکنون روی صفحه باید موارد زیر را مشاهده کنید:

بد نیست فقط با 4 خط HTML و دو واردات!
4. تجسم وظایف
به عنوان گام بعدی، بیایید یک مؤلفه ایجاد کنیم که بتوانیم از آن برای تجسم وظایف در برد kanban استفاده کنیم.
به دایرکتوری src/app بروید و دستور CLI زیر را اجرا کنید:
ng generate component task
این دستور TaskComponent تولید می کند و اعلان آن را به AppModule اضافه می کند. در داخل فهرست task ، فایلی به نام task.ts ایجاد کنید. ما از این فایل برای تعریف رابط وظایف در برد 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 را ویرایش کنید! task.component.html را باز کنید و محتوای آن را با 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 تجسم می کنیم!
در AppComponent آرایه ای به نام todo تعریف کنید و در داخل آن دو کار اضافه کنید:
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!'
}
];
}
اکنون، به پایین app.component.html دستور *ngFor زیر را اضافه کنید:
src/app/app.component.html
<app-task *ngFor="let task of todo" [task]="task"></app-task>
وقتی مرورگر را باز می کنید باید موارد زیر را مشاهده کنید:
5. پیاده سازی کشیدن و رها کردن برای وظایف
ما اکنون برای بخش سرگرم کننده آماده هستیم! بیایید سه swimlane برای سه حالت مختلف کار ایجاد کنیم و با استفاده از Angular CDK، یک قابلیت کشیدن و رها کردن را اجرا کنیم.
در app.component.html ، جزء app-task را با دستور *ngFor در بالا حذف کنید و آن را با:
src/app/app.component.html
<div class="content-wrapper">
<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>
</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>
...
ابتدا، ما swimlane را به عنوان یک mat-card تعریف می کنیم که از دستور cdkDropList استفاده می کند. ما به دلیل سبکهایی که این مؤلفه ارائه میکند، از mat-card استفاده میکنیم. cdkDropList بعداً به ما اجازه می دهد وظایف را در داخل عنصر رها کنیم. ما همچنین دو ورودی زیر را تنظیم می کنیم:
-
cdkDropListData- ورودی لیست دراپ که به ما امکان می دهد آرایه داده را مشخص کنیم -
cdkDropListConnectedTo- ارجاع بهcdkDropListدیگری است کهcdkDropListفعلی به آن متصل است. با تنظیم این ورودی، مشخص می کنیم که در کدام لیست های دیگر می توانیم موارد را رها کنیم
علاوه بر این، ما می خواهیم رویداد drop را با استفاده از خروجی cdkDropListDropped مدیریت کنیم. هنگامی که cdkDropList این خروجی را منتشر می کند، ما متد drop اعلام شده در AppComponent را فراخوانی می کنیم و رویداد فعلی را به عنوان آرگومان ارسال می کنیم.
توجه داشته باشید که ما همچنین یک id برای استفاده به عنوان شناسه برای این کانتینر و یک نام class مشخص می کنیم تا بتوانیم به آن استایل بدهیم. حالا بیایید نگاهی به محتوای فرزندان mat-card . دو عنصری که ما در آنجا داریم عبارتند از:
- یک پاراگراف که وقتی هیچ موردی در لیست
todoوجود ندارد، از آن برای نشان دادن متن "لیست خالی" استفاده می کنیم - جزء
app-task. توجه داشته باشید که در اینجا ما با فراخوانی متدeditTaskبا نام لیست و شی$event، خروجیeditرا که در ابتدا اعلام کرده بودیم، مدیریت می کنیم. این به ما کمک می کند تا کار ویرایش شده را از لیست صحیح جایگزین کنیم. در مرحله بعد، همانطور که در بالا انجام دادیم، روی لیستtodoتکرار می کنیم و ورودیtaskرا ارسال می کنیم. با این حال، این بار دستورcdkDragرا نیز اضافه می کنیم. این باعث می شود وظایف فردی قابل کشیدن باشد.
برای اینکه همه این کارها انجام شود، باید app.module.ts به روز کنیم و یک import به 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[]>): 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 ابتدا بررسی میکنیم که در همان لیستی که وظیفه از آن میآید خارج میشویم. اگر اینطور است، ما بلافاصله برمی گردیم. در غیر این صورت، وظیفه فعلی را به شنای مقصد منتقل می کنیم.
نتیجه باید این باشد:

در این مرحله باید بتوانید موارد را بین دو لیست انتقال دهید!
6. ایجاد وظایف جدید
اکنون، اجازه دهید یک عملکرد برای ایجاد وظایف جدید پیاده سازی کنیم. برای این منظور، بیایید قالب 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 " در کنار برچسب "افزودن وظیفه" اضافه می کنیم. برای قرار دادن دکمه در بالای لیست شناورها به لفاف اضافی نیاز داریم که بعداً با استفاده از فلکس باکس آن را در کنار یکدیگر قرار خواهیم داد. از آنجایی که این دکمه از مولفه دکمه مواد استفاده می کند، باید ماژول مربوطه را در AppModule وارد کنیم:
src/app/app.module.ts
...
import { MatButtonModule } from '@angular/material/button';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
MatButtonModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
اکنون، بیایید عملکرد اضافه کردن وظایف را در AppComponent پیاده سازی کنیم. ما از یک گفتگوی مواد استفاده خواهیم کرد. در گفتگو ما یک فرم با دو فیلد خواهیم داشت: عنوان و توضیحات. هنگامی که کاربر بر روی دکمه "افزودن وظیفه" کلیک می کند، کادر گفتگو را باز می کنیم، و هنگامی که کاربر فرم را ارسال می کند، وظیفه جدید ایجاد شده را به لیست todo اضافه می کنیم.
بیایید به اجرای سطح بالای این قابلیت در AppComponent نگاه کنیم:
src/app/app.component.ts
...
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|undefined) => {
if (!result) {
return;
}
this.todo.push(result.task);
});
}
}
یک سازنده اعلام می کنیم که در آن کلاس MatDialog را تزریق می کنیم. در داخل newTask ما:
- یک گفتگوی جدید را با استفاده از
TaskDialogComponentباز کنید که کمی بعد آن را تعریف خواهیم کرد. - مشخص کنید که می خواهیم دیالوگ
270px. - یک کار خالی را به عنوان داده به دیالوگ منتقل کنید. در
TaskDialogComponentمیتوانیم مرجعی برای این شی داده دریافت کنیم. - ما در رویداد بسته مشترک می شویم و وظیفه را از شی
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 dialog در AppComponent ارسال میکنیم. برای به روز رسانی عنوان و شرح کار زمانی که کاربر محتوای فیلدهای مربوطه را تغییر می دهد، از اتصال داده دو طرفه با ngModel استفاده می کنیم.
هنگامی که کاربر روی دکمه OK کلیک می کند، ما به طور خودکار نتیجه { 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 اعلانهای زیر را به پایین فایل اضافه کنید:
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 { }
هنگامی که اکنون روی دکمه "افزودن وظیفه" کلیک می کنید، باید رابط کاربری زیر را مشاهده کنید:

7. بهبود سبک های برنامه
برای جذابتر کردن برنامه از نظر بصری، طرحبندی آن را با کمی تغییر سبکهای آن بهبود خواهیم داد. ما می خواهیم شناورها را در کنار یکدیگر قرار دهیم. ما همچنین تنظیمات جزئی دکمه "افزودن وظیفه" و برچسب لیست خالی را می خواهیم.
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، swimlanes را در کنار یکدیگر قرار می دهیم و در نهایت تنظیماتی را در نحوه تجسم وظایف و لیست های خالی انجام می دهیم.
پس از بارگیری مجدد برنامه، باید رابط کاربری زیر را مشاهده کنید:

اگرچه ما سبک های برنامه خود را به طور قابل توجهی بهبود بخشیم، اما همچنان هنگام جابجایی وظایف با یک مشکل آزاردهنده روبرو هستیم:

وقتی کار "خرید شیر" را شروع می کنیم، دو کارت برای همان کار می بینیم - کارتی که می کشیم و کارتی که در swimlane است. Angular CDK نام کلاس های CSS را در اختیار ما قرار می دهد که می توانیم برای رفع این مشکل از آنها استفاده کنیم.
تغییر سبک زیر را به پایین src/app/app.component.css اضافه کنید:
src/app/app.component.css
.cdk-drag-animating {
transition: transform 250ms;
}
.cdk-drag-placeholder {
opacity: 0;
}
در حالی که یک عنصر را میکشیم، کشیدن و رها کردن Angular CDK آن را شبیهسازی میکند و در موقعیتی قرار میدهد که میخواهیم اصل را رها کنیم. برای اطمینان از اینکه این عنصر قابل مشاهده نیست، ویژگی opacity را در کلاس 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;
}
8. ویرایش و حذف وظایف موجود
برای ویرایش و حذف وظایف موجود، از بیشتر قابلیتهایی که قبلاً اجرا کردهایم دوباره استفاده میکنیم! وقتی کاربر روی یک کار دوبار کلیک میکند، 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>
این دکمه نماد حذف مواد را نشان می دهد. هنگامی که کاربر روی آن کلیک میکند، دیالوگ را میبندیم و در نتیجه شیء را به معنای واقعی کلمه { 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|undefined) => {
if (!result) {
return;
}
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تنظیم می شود (یعنی زمانی که کاربر دکمه حذف را فشار داده است)، وظیفه را از لیست مربوطه حذف می کنیم. - از طرف دیگر، ما فقط وظیفهای را که در ایندکس داده شده است با وظیفهای که از نتیجه گفتگو دریافت کردیم جایگزین میکنیم.
9. ایجاد یک پروژه Firebase جدید
حالا بیایید یک پروژه Firebase جدید ایجاد کنیم!
- به کنسول Firebase بروید.
- یک پروژه جدید با نام "KanbanFire" ایجاد کنید.
10. افزودن Firebase به پروژه
در این بخش ما پروژه خود را با Firebase ادغام خواهیم کرد! تیم Firebase بسته @angular/fire را ارائه میدهد که یکپارچگی بین دو فناوری را فراهم میکند. برای افزودن پشتیبانی Firebase به برنامه خود، دایرکتوری root فضای کاری خود را باز کرده و اجرا کنید:
ng add @angular/fire
این دستور بسته @angular/fire را نصب می کند و چند سوال از شما می پرسد. در ترمینال خود، باید چیزی شبیه به:

در همین حال، نصب یک پنجره مرورگر باز میکند تا بتوانید با حساب Firebase خود احراز هویت کنید. در نهایت، از شما می خواهد که یک پروژه Firebase را انتخاب کنید و چند فایل روی دیسک شما ایجاد می کند.
بعد، ما باید یک پایگاه داده Firestore ایجاد کنیم! در بخش «Cloud Firestore» روی «ایجاد پایگاه داده» کلیک کنید.

پس از آن، یک پایگاه داده در حالت تست ایجاد کنید:

در نهایت یک منطقه را انتخاب کنید:

اکنون تنها چیزی که باقی مانده است اضافه کردن پیکربندی Firebase به محیط خود است. می توانید پیکربندی پروژه خود را در Firebase Console پیدا کنید.
- روی نماد چرخ دنده در کنار نمای کلی پروژه کلیک کنید.
- تنظیمات پروژه را انتخاب کنید.

در بخش «برنامههای شما»، «برنامه وب» را انتخاب کنید:

در مرحله بعد، برنامه خود را ثبت کنید و مطمئن شوید که "Firebase Hosting" را فعال کرده اید :

پس از کلیک بر روی "ثبت برنامه"، می توانید پیکربندی خود را در src/environments/environment.ts کپی کنید:
در پایان، فایل پیکربندی شما باید به شکل زیر باشد:
src/environments/environment.ts
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>'
}
};
11. انتقال داده ها به Firestore
اکنون که Firebase SDK را راهاندازی کردهایم، از @angular/fire برای انتقال دادههایمان به Firestore استفاده میکنیم! ابتدا، اجازه دهید ماژول های مورد نیاز خود را در AppModule وارد کنیم:
src/app/app.module.ts
...
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 {}
از آنجایی که از Firestore استفاده خواهیم کرد، باید AngularFirestore در سازنده AppComponent تزریق کنیم:
src/app/app.component.ts
...
import { AngularFirestore } from '@angular/fire/firestore';
@Component({...})
export class AppComponent {
...
constructor(private dialog: MatDialog, private store: AngularFirestore) {}
...
}
در مرحله بعد، روش اولیه سازی آرایه های swimlane را به روز می کنیم:
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 به جای آرایه، یک قابل مشاهده را برمی گرداند، و همچنین مشخص می کنیم که فیلد id برای اسناد این مجموعه باید id نامیده شود تا با نامی که در رابط Task استفاده می کنیم مطابقت داشته باشد. قابل مشاهده ای که توسط valueChanges برگردانده می شود هر زمان که تغییر کند مجموعه ای از وظایف را منتشر می کند.
از آنجایی که ما به جای آرایهها با قابل مشاهدهها کار میکنیم، باید نحوه اضافه کردن، حذف و ویرایش کارها و قابلیت جابجایی وظایف بین مسیرهای شنا را به روز کنیم. به جای جهش آرایه های درون حافظه خود، از 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
);
}
در قطعه بالا، کد جدید برجسته شده است. برای انتقال یک کار از swimlane فعلی به یک هدف، میخواهیم وظیفه را از مجموعه اول حذف کرده و به مجموعه دوم اضافه کنیم. از آنجایی که ما دو عملیات را انجام می دهیم که می خواهیم شبیه به یکی شوند (یعنی عملیات را اتمی کنیم)، آنها را در یک تراکنش Firestore اجرا می کنیم.
بعد، بیایید روش editTask را برای استفاده از Firestore به روز کنیم! در داخل محاوره بسته، باید خطوط کد زیر را تغییر دهیم:
src/app/app.component.ts
...
dialogRef.afterClosed().subscribe((result: TaskDialogResult|undefined) => {
if (!result) {
return;
}
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) جایگزین کنید.
توجه داشته باشید که اکنون مجموعه های ما آرایه نیستند، بلکه قابل مشاهده هستند. برای اینکه بتوانیم آنها را تجسم کنیم، باید الگوی AppComponent را به روز کنیم. فقط هر دسترسی از ویژگی های todo ، inProgress و done را با todo | async جایگزین کنید todo | async , inProgress | async و done | async به ترتیب done | async
لوله async به طور خودکار مشترک مشاهدات مرتبط با مجموعه ها می شود. هنگامی که مشاهده پذیرها یک مقدار جدید منتشر می کنند، Angular به طور خودکار تشخیص تغییر را اجرا می کند و آرایه منتشر شده را پردازش می کند.
به عنوان مثال، اجازه دهید تغییراتی را که باید در todo swimlane ایجاد کنیم، بررسی کنیم:
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>
هنگامی که داده ها را به دستور cdkDropList ارسال می کنیم، لوله async را اعمال می کنیم. در داخل دستور *ngIf هم همینطور است، اما توجه داشته باشید که در آنجا از زنجیره اختیاری (همچنین به عنوان اپراتور ناوبری ایمن در Angular نیز شناخته میشود)، هنگام دسترسی به ویژگی length استفاده میکنیم تا مطمئن شویم در صورت todo | async null یا undefined نیست.
حالا وقتی یک کار جدید در رابط کاربری ایجاد می کنید و Firestore را باز می کنید، باید چیزی شبیه به این را ببینید:

12. بهبود به روز رسانی های خوش بینانه
در برنامه ما در حال حاضر در حال انجام به روز رسانی های خوش بینانه هستیم. ما منبع حقیقت خود را در Firestor داریم، اما در عین حال کپی محلی از وظایف داریم. هنگامی که هر یک از مشاهدات مرتبط با مجموعه ها منتشر می شود، مجموعه ای از وظایف را دریافت می کنیم. هنگامی که یک اقدام کاربر حالت را تغییر می دهد، ابتدا مقادیر محلی را به روز می کنیم و سپس تغییر را به Firestore منتشر می کنیم.
هنگامی که یک کار را از یک swimlane به دیگری منتقل می کنیم، transferArrayItem, فراخوانی می کنیم که بر روی نمونه های محلی آرایه هایی که وظایف هر شناگر را نشان می دهد عمل می کند. Firebase SDK با این آرایهها بهعنوان غیرقابل تغییر برخورد میکند، به این معنی که دفعه بعد که Angular تشخیص تغییر را اجرا میکند، نمونههای جدیدی از آنها را دریافت میکنیم که قبل از انتقال وظیفه، حالت قبلی را ارائه میکنند.
در همان زمان، ما یک بهروزرسانی Firestore را راهاندازی میکنیم و Firebase SDK یک بهروزرسانی با مقادیر صحیح را راهاندازی میکند، بنابراین در چند میلیثانیه رابط کاربری به حالت صحیح خود میرسد. این باعث می شود وظیفه ای که ما به تازگی منتقل کردیم از لیست اول به لیست بعدی بپرد. شما می توانید این را به خوبی در GIF زیر مشاهده کنید:

راه درست برای حل این مشکل از برنامهای به برنامه دیگر متفاوت است، اما در همه موارد باید اطمینان حاصل کنیم که تا زمان بهروزرسانی دادههایمان وضعیت ثابتی را حفظ میکنیم.
ما میتوانیم از BehaviorSubject استفاده کنیم، که مشاهدهگر اصلی را که از 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 ایجاد می کنیم که هر بار که مشاهده پذیر مرتبط با مجموعه تغییر می کند یک مقدار منتشر می کند.
همه چیز همانطور که انتظار می رود کار می کند، زیرا BehaviorSubject از آرایه در سراسر فراخوان های تشخیص تغییر استفاده می کند و تنها زمانی به روز می شود که مقدار جدیدی از Firestore دریافت کنیم.
13. استقرار برنامه
تنها کاری که برای استقرار برنامه خود باید انجام دهیم این است که:
ng deploy
این دستور خواهد شد:
- برنامه خود را با پیکربندی تولید آن، با استفاده از بهینهسازیهای زمان کامپایل بسازید.
- برنامه خود را در میزبانی Firebase مستقر کنید.
- یک URL خروجی بگیرید تا بتوانید نتیجه را پیشنمایش کنید.
14. تبریک می گویم
تبریک می گویم، شما با موفقیت یک برد کانبان با Angular و Firebase ساخته اید!
شما یک رابط کاربری با سه ستون ایجاد کردید که وضعیت وظایف مختلف را نشان می دهد. با استفاده از Angular CDK، کشیدن و رها کردن وظایف را در ستون ها پیاده سازی کردید. سپس، با استفاده از متریال Angular، فرمی برای ایجاد وظایف جدید و ویرایش کارهای موجود ایجاد کردید. سپس، نحوه استفاده از @angular/fire را یاد گرفتید و تمام وضعیت برنامه را به Firestore منتقل کردید. در نهایت، شما برنامه خود را در Firebase Hosting مستقر کردید.
بعدش چی؟
به یاد داشته باشید که ما برنامه را با استفاده از تنظیمات آزمایشی مستقر کردیم. قبل از استقرار برنامه خود در تولید، مطمئن شوید که مجوزهای صحیح را تنظیم کرده اید. در اینجا می توانید نحوه انجام این کار را بیاموزید.
در حال حاضر، ما ترتیب وظایف فردی را در یک شناگر خاص حفظ نمی کنیم. برای پیاده سازی این کار می توانید از یک فیلد سفارش در سند وظیفه استفاده کنید و بر اساس آن مرتب سازی کنید.
علاوه بر این، ما برد kanban را فقط برای یک کاربر ساختهایم، به این معنی که برای هر کسی که برنامه را باز میکند، یک برد kanban داریم. برای پیاده سازی تابلوهای جداگانه برای کاربران مختلف برنامه خود، باید ساختار پایگاه داده خود را تغییر دهید. در اینجا با بهترین شیوه های Firestore آشنا شوید.

