ساخت وب اپلیکیشن با Angular و Firebase

1. مقدمه

آخرین به روز رسانی: 2020-09-11

چیزی که خواهی ساخت

در این کد لبه، ما یک برد وب کانبان با Angular و Firebase می سازیم! برنامه نهایی ما دارای سه دسته کار است: عقب ماندگی، در حال انجام و تکمیل شده. ما قادر خواهیم بود با کشیدن و رها کردن، وظایفی را ایجاد، حذف و از یک دسته به دسته دیگر منتقل کنیم.

ما رابط کاربری را با استفاده از Angular توسعه خواهیم داد و از Firestore به عنوان فروشگاه دائمی خود استفاده خواهیم کرد. در انتهای کد لبه، برنامه را با استفاده از Angular CLI در Firebase Hosting مستقر خواهیم کرد.

b23bd3732d0206b.png

چیزی که یاد خواهید گرفت

  • نحوه استفاده از متریال 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 را باز کنید و باید خروجی مشابهی ببینید:

5ede7bc5b1109bf3.png

در ویرایشگر خود، 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 وارد کنیم.

اکنون روی صفحه باید موارد زیر را مشاهده کنید:

a39cf8f8428a03bc.png

بد نیست فقط با 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>

وقتی مرورگر را باز می کنید باید موارد زیر را مشاهده کنید:

d96fccd13c63ceb1.png

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 - ارجاع به cdkDropList cdkDropList است که 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 ابتدا بررسی می‌کنیم که در همان لیستی که وظیفه از آن می‌آید خارج می‌شویم. اگر اینطور است، ما بلافاصله برمی گردیم. در غیر این صورت، وظیفه فعلی را به شنای مقصد منتقل می کنیم.

نتیجه باید این باشد:

460f86bcd10454cf.png

در این مرحله باید بتوانید موارد را بین دو لیست انتقال دهید!

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 { }

هنگامی که اکنون روی دکمه "افزودن وظیفه" کلیک می کنید، باید رابط کاربری زیر را مشاهده کنید:

33bcb987fade2a87.png

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، شناورها را در کنار یکدیگر قرار می دهیم و در نهایت تنظیماتی را در نحوه تجسم وظایف و لیست های خالی انجام می دهیم.

پس از بارگیری مجدد برنامه، باید رابط کاربری زیر را مشاهده کنید:

69225f0b1aa5cb50.png

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

f9aae712027624af.png

وقتی کار "خرید شیر" را شروع می کنیم، دو کارت برای همان کار می بینیم - کارتی که می کشیم و کارتی که در 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 را نصب می کند و چند سوال از شما می پرسد. در ترمینال خود، باید چیزی شبیه به:

9ba88c0d52d18d0.png

در همین حال، نصب یک پنجره مرورگر باز می‌کند تا بتوانید با حساب Firebase خود احراز هویت کنید. در نهایت، از شما می خواهد که یک پروژه Firebase را انتخاب کنید و چند فایل روی دیسک شما ایجاد می کند.

بعد، ما باید یک پایگاه داده Firestore ایجاد کنیم! در بخش «Cloud Firestore» روی «ایجاد پایگاه داده» کلیک کنید.

1e4a08b5a2462956.png

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

ac1181b2c32049f9.png

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

34bb94cc542a0597.png

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

  • روی نماد چرخ دنده در کنار نمای کلی پروژه کلیک کنید.
  • تنظیمات پروژه را انتخاب کنید.

c8253a20031de8a9.png

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

428a1abcd0f90b23.png

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

586e44cb27dd8f39.png

پس از کلیک بر روی "ثبت برنامه"، می توانید پیکربندی خود را در src/environments/environment.ts کپی کنید:

e30f142d79cecf8f.png

در پایان، فایل پیکربندی شما باید به شکل زیر باشد:

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 را باز می‌کنید، باید چیزی شبیه به این ببینید:

dd7ee20c0a10ebe2.png

12. بهبود به روز رسانی های خوش بینانه

در برنامه ما در حال حاضر به روز رسانی های خوش بینانه را انجام می دهیم. ما منبع حقیقت خود را در Firestor داریم، اما در عین حال کپی محلی از وظایف داریم. وقتی هر یک از مشاهدات مرتبط با مجموعه ها منتشر می شود، مجموعه ای از وظایف را دریافت می کنیم. هنگامی که یک اقدام کاربر حالت را تغییر می دهد، ابتدا مقادیر محلی را به روز می کنیم و سپس تغییر را به Firestore منتشر می کنیم.

هنگامی که یک کار را از یک swimlane به دیگری منتقل می کنیم transferArrayItem, فراخوانی می کنیم که بر روی نمونه های محلی آرایه هایی که وظایف هر شناگر را نشان می دهد عمل می کند. Firebase SDK با این آرایه‌ها به‌عنوان غیرقابل تغییر برخورد می‌کند، به این معنی که دفعه بعد که Angular تشخیص تغییر را اجرا می‌کند، نمونه‌های جدیدی از آن‌ها را دریافت می‌کنیم که قبل از انتقال وظیفه، حالت قبلی را ارائه می‌کنند.

در همان زمان، ما یک به‌روزرسانی Firestore را راه‌اندازی می‌کنیم و Firebase SDK یک به‌روزرسانی با مقادیر صحیح را راه‌اندازی می‌کند، بنابراین در چند میلی‌ثانیه رابط کاربری به حالت صحیح خود می‌رسد. این باعث می شود وظیفه ای که ما به تازگی منتقل کردیم از لیست اول به لیست بعدی بپرد. شما می توانید این را به خوبی در GIF زیر مشاهده کنید:

70b946eebfa6f316.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

این دستور خواهد شد:

  1. برنامه خود را با پیکربندی تولید آن، با استفاده از بهینه‌سازی‌های زمان کامپایل بسازید.
  2. برنامه خود را در میزبانی Firebase مستقر کنید.
  3. یک URL تولید کنید تا بتوانید پیش نمایش نتیجه را مشاهده کنید.

14. تبریک می گویم

تبریک می‌گوییم، شما با موفقیت یک برد کانبان با Angular و Firebase ساخته‌اید!

شما یک رابط کاربری با سه ستون ایجاد کردید که وضعیت وظایف مختلف را نشان می دهد. با استفاده از Angular CDK، کشیدن و رها کردن وظایف را در ستون ها پیاده سازی کردید. سپس با استفاده از متریال Angular، فرمی برای ایجاد وظایف جدید و ویرایش کارهای موجود ایجاد کردید. در مرحله بعد، نحوه استفاده از @angular/fire را یاد گرفتید و تمام وضعیت برنامه را به Firestore منتقل کردید. در نهایت، شما برنامه خود را در Firebase Hosting مستقر کردید.

بعدش چی؟

به یاد داشته باشید که ما برنامه را با استفاده از تنظیمات آزمایشی مستقر کردیم. قبل از استقرار برنامه خود در تولید، مطمئن شوید که مجوزهای صحیح را تنظیم کرده اید. در اینجا می توانید نحوه انجام این کار را بیاموزید.

در حال حاضر، ما ترتیب وظایف فردی را در یک شناگر خاص حفظ نمی کنیم. برای پیاده سازی این کار می توانید از یک فیلد سفارش در سند وظیفه استفاده کنید و بر اساس آن مرتب سازی کنید.

علاوه بر این، ما برد kanban را فقط برای یک کاربر ساخته‌ایم، به این معنی که برای هر کسی که برنامه را باز می‌کند، یک برد kanban داریم. برای پیاده سازی تابلوهای جداگانه برای کاربران مختلف برنامه خود، باید ساختار پایگاه داده خود را تغییر دهید. در اینجا با بهترین شیوه های Firestore آشنا شوید.