1. مقدمه
آخرین به روز رسانی: 2020-09-11
چیزی که خواهی ساخت
در این کد لبه، ما یک برد وب کانبان با Angular و Firebase می سازیم! برنامه نهایی ما دارای سه دسته کار است: عقب ماندگی، در حال انجام و تکمیل شده. ما قادر خواهیم بود با کشیدن و رها کردن، وظایفی را ایجاد، حذف و از یک دسته به دسته دیگر منتقل کنیم.
ما رابط کاربری را با استفاده از Angular توسعه خواهیم داد و از Firestore به عنوان فروشگاه دائمی خود استفاده خواهیم کرد. در انتهای کد لبه، برنامه را با استفاده از Angular CLI در Firebase Hosting مستقر خواهیم کرد.
چیزی که یاد خواهید گرفت
- نحوه استفاده از متریال Angular و CDK.
- چگونه یکپارچه سازی Firebase را به برنامه Angular خود اضافه کنید.
- چگونه داده های دائمی خود را در Firestore نگه دارید.
- چگونه برنامه خود را با استفاده از Angular CLI با یک دستور در Firebase Hosting مستقر کنید.
آنچه شما نیاز دارید
این کد لبه فرض می کند که شما یک حساب 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 با اجرای اجزای رابط کاربری سازگار با طراحی متریال که بخشی از بسته @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="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>
...
ابتدا، ما swimlane را به عنوان یک mat-card
تعریف می کنیم که از دستور cdkDropList
استفاده می کند. ما به دلیل سبکهایی که این مؤلفه ارائه میکند mat-card
استفاده میکنیم. cdkDropList
بعداً به ما اجازه می دهد وظایف را در داخل عنصر رها کنیم. ما همچنین دو ورودی زیر را تنظیم می کنیم:
-
cdkDropListData
- ورودی لیست دراپ که به ما امکان می دهد آرایه داده را مشخص کنیم -
cdkDropListConnectedTo
- ارجاع به cdkDropListcdkDropList
است کهcdkDropList
فعلی به آن متصل است. با تنظیم این ورودی، مشخص می کنیم که در کدام لیست های دیگر می توانیم موارد را رها کنیم
علاوه بر این، میخواهیم رویداد drop را با استفاده از خروجی cdkDropListDropped
. هنگامی که cdkDropList
این خروجی را منتشر کرد، ما متد drop
اعلام شده در AppComponent
را فراخوانی می کنیم و رویداد فعلی را به عنوان آرگومان ارسال می کنیم.
توجه داشته باشید که ما همچنین یک id
برای استفاده به عنوان شناسه برای این کانتینر و یک نام class
مشخص می کنیم تا بتوانیم به آن استایل بدهیم. حالا بیایید نگاهی به محتوای فرزندان mat-card
بیندازیم. دو عنصری که در آنجا داریم عبارتند از:
- یک پاراگراف، که وقتی هیچ موردی در لیست
todo
کار وجود ندارد، از آن برای نشان دادن متن "لیست خالی" استفاده می کنیم - جزء
app-task
. توجه داشته باشید که در اینجا ما با فراخوانیeditTask
edit
نام لیست و شی$event
، خروجی ویرایشی را که در ابتدا اعلام کرده بودیم، مدیریت می کنیم. این به ما کمک می کند تا کار ویرایش شده را از لیست صحیح جایگزین کنیم. در مرحله بعد، همانطور که در بالا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[]|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
ابتدا بررسی میکنیم که در همان لیستی که وظیفه از آن میآید خارج میشویم. اگر اینطور است، ما بلافاصله برمی گردیم. در غیر این صورت، وظیفه فعلی را به شنای مقصد منتقل می کنیم.
نتیجه باید این باشد:
در این مرحله باید بتوانید موارد را بین دو لیست انتقال دهید!
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
کنید که کمی بعد آن را تعریف خواهیم کرد. - مشخص کنید که می خواهیم دیالوگ 270 پیکسل عرض داشته
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، شناورها را در کنار یکدیگر قرار می دهیم و در نهایت تنظیماتی را در نحوه تجسم وظایف و لیست های خالی انجام می دهیم.
پس از بارگیری مجدد برنامه، باید رابط کاربری زیر را مشاهده کنید:
اگرچه ما سبک های برنامه خود را به طور قابل توجهی بهبود بخشیم، اما هنوز هنگام جابجایی وظایف با یک مشکل آزاردهنده روبرو هستیم:
وقتی کار "خرید شیر" را شروع می کنیم، دو کارت برای همان کار می بینیم - کارتی که می کشیم و کارتی که در 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 پیدا کنید.
- روی نماد چرخ دنده در کنار نمای کلی پروژه کلیک کنید.
- تنظیمات پروژه را انتخاب کنید.
در بخش «برنامههای شما»، «برنامه وب» را انتخاب کنید:
سپس، برنامه خود را ثبت کنید و مطمئن شوید که "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)
result.task) جایگزین کنید.
توجه داشته باشید که اکنون مجموعه های ما آرایه نیستند، بلکه قابل مشاهده هستند. برای اینکه بتوانیم آنها را تجسم کنیم، باید الگوی AppComponent
را به روز کنیم. فقط هر دسترسی از ویژگی های todo
، inProgress
و done
را با todo | async
جایگزین کنید todo | async
, inProgress | async
و done | async
به ترتیب done | 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 آشنا شوید.