Xây dựng ứng dụng web với Angular và Firebase

Sử dụng bộ sưu tập để sắp xếp ngăn nắp các trang Lưu và phân loại nội dung dựa trên lựa chọn ưu tiên của bạn.

1. Giới thiệu

Lần cập nhật gần đây nhất: Ngày 11 tháng 9 năm 2020

Sản phẩm bạn sẽ tạo ra

Trong lớp học lập trình này, chúng ta sẽ xây dựng một bảng kanban trên web với Angular và Firebase! Ứng dụng cuối cùng của chúng ta sẽ có 3 danh mục nhiệm vụ: tạm dừng, đang tiến hành và đã hoàn thành. Chúng tôi có thể tạo, xóa và chuyển việc cần làm từ danh mục này sang danh mục khác bằng cách kéo và thả.

Chúng tôi sẽ phát triển giao diện người dùng bằng cách sử dụng Angular và dùng Firestore làm cửa hàng ổn định. Ở cuối lớp học lập trình, chúng ta sẽ triển khai ứng dụng này vào tính năng Lưu trữ Firebase bằng giao diện dòng lệnh (CLI) Angular.

b23bd3732d0206b.png

Kiến thức bạn sẽ học được

  • Cách sử dụng vật liệu Angular và CDK.
  • Cách thêm tính năng tích hợp Firebase vào ứng dụng Angular.
  • Cách giữ dữ liệu ổn định của bạn trong Firestore.
  • Cách triển khai ứng dụng của bạn vào Lưu trữ Firebase bằng cách sử dụng CLI Angular với một lệnh duy nhất.

Bạn cần có

Lớp học mã này giả định rằng bạn có Tài khoản Google và kiến thức cơ bản về Angular và Angular CLI.

Hãy bắt đầu!

2. Tạo dự án mới

Trước tiên, hãy tạo một không gian làm việc Angular mới:

ng new kanban-fire
? Would you like to add Angular routing? No
? Which stylesheet format would you like to use? CSS

Bước này có thể mất vài phút. CLI Angular tạo cấu trúc dự án của bạn và cài đặt tất cả các phần phụ thuộc. Khi quá trình cài đặt hoàn tất, hãy chuyển đến thư mục kanban-fire và khởi động máy chủ phát triển của Angular CLI\39:

ng serve

Mở http://localhost:4200 và bạn sẽ thấy một kết quả tương tự như sau:

5ede7bc5b1109bf3.png.

Trong trình chỉnh sửa, hãy mở src/app/app.component.html và xóa toàn bộ nội dung của trình chỉnh sửa đó. Khi quay lại http://localhost:4200 bạn sẽ thấy một trang trống.

3. Thêm Material và CDK

Angular có sẵn một phương pháp triển khai các thành phần giao diện người dùng tuân thủ thiết kế Material Design trong gói @angular/material. Một trong các phần phụ thuộc của @angular/materialBộ phát triển thành phần hay CDK. CDK cung cấp các tính năng gốc, chẳng hạn như tiện ích a11y, kéo và thả và lớp phủ. Chúng tôi phân phối CDK trong gói @angular/cdk.

Để thêm tài liệu vào ứng dụng, hãy làm như sau:

ng add @angular/material

Lệnh này yêu cầu bạn chọn một giao diện, nếu bạn muốn dùng kiểu kiểu tài liệu toàn cầu và nếu bạn muốn thiết lập ảnh động cho trình duyệt cho Angular Material. Chọn "Indigo/Pink" để nhận được kết quả tương tự như trong lớp học lập trình này và trả lời với "Yes" cho hai câu hỏi cuối.

Lệnh ng add cài đặt @angular/material, các phần phụ thuộc và nhập BrowserAnimationsModule vào AppModule. Trong bước tiếp theo, chúng ta có thể bắt đầu sử dụng các thành phần mà mô-đun này cung cấp!

Trước tiên, hãy thêm thanh công cụ và một biểu tượng vào AppComponent. Mở app.component.html và thêm mã đánh dấu sau:

src/app/app.component.html

<mat-toolbar color="primary">
  <mat-icon>local_fire_department</mat-icon>
  <span>Kanban Fire</span>
</mat-toolbar>

Ở đây, chúng ta thêm một thanh công cụ sử dụng màu chính của giao diện thiết kế Material Design và bên trong thanh này, chúng ta dùng biểu tượng local_fire_depeartment bên cạnh nhãn "Kanban Fire;" Nếu nhìn vào bảng điều khiển ngay bây giờ, bạn sẽ thấy Angular gửi một vài lỗi. Để khắc phục vấn đề này, hãy nhớ thêm những mục nhập sau đây vào 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 { }

Vì chúng ta dùng thanh công cụ và biểu tượng tài liệu Angular, nên chúng ta cần nhập các mô-đun tương ứng vào AppModule.

Bây giờ, trên màn hình, bạn sẽ thấy:

a39cf8f8428a03bc.png

Không tệ chỉ với 4 dòng HTML và 2 nhập!

4. Trực quan hoá nhiệm vụ

Bước tiếp theo, hãy tạo một thành phần mà chúng ta có thể sử dụng để hình ảnh hóa các tác vụ trong bảng kanban.

Chuyển đến thư mục src/app và chạy lệnh CLI sau đây:

ng generate component task

Lệnh này tạo ra TaskComponent và thêm phần khai báo của lệnh này vào AppModule. Trong thư mục task, hãy tạo một tệp có tên là task.ts. Chúng tôi sẽ sử dụng tệp này để xác định giao diện của các tác vụ trong bảng kanban. Mỗi công việc sẽ có một trường id, titledescription (không bắt buộc) cho toàn bộ chuỗi loại:

src/app/task/task.ts

export interface Task {
  id?: string;
  title: string;
  description: string;
}

Bây giờ, hãy cập nhật task.component.ts. Chúng ta muốn TaskComponent chấp nhận làm dữ liệu đầu vào của đối tượng thuộc loại Task và chúng ta muốn tệp đó có thể phát ra "edit" đầu ra:

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>();
}

Chỉnh sửa mẫu của TaskComponent! Mở task.component.html và thay thế nội dung của trang web bằng HTML sau:

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>

Xin lưu ý rằng chúng tôi hiện đang gặp lỗi trong bảng điều khiển:

'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

Trong mẫu ở trên, chúng tôi đang sử dụng thành phần mat-card từ @angular/material, nhưng chúng tôi chưa nhập mô-đun tương ứng trong ứng dụng. Để khắc phục lỗi ở trên, chúng tôi cần nhập MatCardModule vào AppModule:

src/app/app.module.ts

...
import { MatCardModule } from '@angular/material/card';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    ...
    MatCardModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Tiếp theo, chúng ta sẽ tạo một vài tác vụ trong AppComponent và trực quan hóa các tác vụ đó bằng TaskComponent!

Trong AppComponent, hãy xác định một mảng có tên là todo và bên trong mảng đó, thêm 2 việc cần làm:

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!'
    }
  ];
}

Bây giờ, ở cuối app.component.html, hãy thêm lệnh *ngFor sau:

src/app/app.component.html

<app-task *ngFor="let task of todo" [task]="task"></app-task>

Khi mở trình duyệt, bạn sẽ thấy những nội dung sau:

d96fccd13c63ceb1.png

5. Triển khai tính năng kéo và thả để thực hiện việc cần làm

Bây giờ, chúng tôi đã sẵn sàng cho phần thú vị này! Hãy tạo ba làn nước cho ba nhiệm vụ khác nhau của tiểu bang và sử dụng CDK của Angular, triển khai chức năng kéo và thả.

Trong app.component.html, hãy xóa thành phần app-task có lệnh *ngFor ở trên cùng và thay thế bằng:

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>

Ở đó có rất nhiều việc đang diễn ra. Hãy xem từng phần của đoạn mã này theo từng bước. Đây là cấu trúc cấp cao nhất của mẫu:

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>

Ở đây, chúng ta tạo một div bao gồm cả 3 làn đường, với tên lớp "container-wrapper." Mỗi bể bơi đều có tên lớp "container" và một tiêu đề bên trong thẻ h2.

Bây giờ, hãy xem cấu trúc của đường bơi đầu tiên:

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

Đầu tiên, chúng ta xác định làn đường là mat-card, sử dụng lệnh cdkDropList. Chúng ta dùng mat-card vì những kiểu mà thành phần này cung cấp. Sau đó, cdkDropList sẽ cho phép chúng ta thả các việc cần làm bên trong phần tử. Chúng tôi cũng đặt hai giá trị nhập vào sau:

  • cdkDropListData – dữ liệu đầu vào trong danh sách thả xuống cho phép chúng ta chỉ định mảng dữ liệu
  • cdkDropListConnectedTo – tham chiếu đến các cdkDropList khác mà cdkDropList hiện tại kết nối. Khi đặt đầu vào này, chúng tôi sẽ chỉ định những danh sách khác mà chúng tôi có thể thả mục vào

Ngoài ra, chúng ta muốn xử lý sự kiện sụt giảm bằng cách sử dụng kết quả cdkDropListDropped. Sau khi cdkDropList phát ra kết quả này, chúng ta sẽ gọi phương thức drop được khai báo bên trong AppComponent và chuyển sự kiện hiện tại làm đối số.

Xin lưu ý rằng chúng tôi cũng chỉ định id để sử dụng làm giá trị nhận dạng cho vùng chứa này và tên class để chúng tôi có thể tạo kiểu cho vùng chứa đó. Bây giờ, hãy xem nội dung con của mat-card. Chúng tôi có hai phần tử sau đây:

  • Một đoạn văn mà chúng tôi dùng để hiển thị "Danh sách trống" văn bản khi không có mục nào trong danh sách todo
  • Thành phần app-task. Xin lưu ý rằng ở đây, chúng ta đang xử lý kết quả edit mà chúng ta đã khai báo ban đầu bằng cách gọi phương thức editTask có tên danh sách và đối tượng $event. Thao tác này sẽ giúp chúng tôi thay thế việc cần làm đã chỉnh sửa trong danh sách chính xác. Tiếp theo, chúng ta lặp lại danh sách todo như đã làm ở trên và chuyển thông tin đầu vào task. Tuy nhiên, lần này chúng ta cũng thêm lệnh cdkDrag. Thanh này giúp bạn có thể kéo từng việc cần làm.

Để thực hiện tất cả việc này, chúng ta cần phải cập nhật app.module.ts và bao gồm một lần nhập vào DragDropModule:

src/app/app.module.ts

...
import { DragDropModule } from '@angular/cdk/drag-drop';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    ...
    DragDropModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Chúng ta cũng cần khai báo mảng inProgressdone, cùng với phương thức editTaskdrop:

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
    );
  }
}

Xin lưu ý rằng trong phương thức drop, trước tiên, chúng ta sẽ kiểm tra để đảm bảo rằng chúng ta đang xem danh sách giống với việc cần làm. Nếu trường hợp đó xảy ra, chúng tôi sẽ ngay lập tức quay lại. Nếu không, chúng tôi sẽ chuyển nhiệm vụ hiện tại sang đường bơi đích.

Kết quả sẽ là:

460f86bcd10454cf.png

Lúc này, bạn đã có thể chuyển các mục giữa hai danh sách!

6. Tạo việc mới cần làm

Bây giờ, hãy triển khai chức năng tạo việc cần làm mới. Vì mục đích này, hãy cập nhật mẫu của 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>

Chúng tôi tạo phần tử div cấp cao nhất xung quanh container-wrapper và thêm một nút có biểu tượng "add" biểu tượng tài liệu bên cạnh nhãn "Add Tasks." Chúng ta cần có trình bao bọc bổ sung để đặt nút ở đầu danh sách làn đường, sau đó chúng ta sẽ đặt chúng bên cạnh nhau bằng hộp linh hoạt. Vì nút này sử dụng thành phần nút Material, nên chúng tôi cần nhập mô-đun tương ứng vào AppModule:

src/app/app.module.ts

...
import { MatButtonModule } from '@angular/material/button';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    ...
    MatButtonModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Bây giờ, hãy triển khai chức năng để thêm việc cần làm trong AppComponent. Chúng tôi sẽ sử dụng hộp thoại Material. Trong hộp thoại, chúng tôi sẽ có một biểu mẫu có hai trường: tiêu đề và mô tả. Khi người dùng nhấp vào nút "Thêm việc cần làm; chúng tôi sẽ mở hộp thoại và khi người dùng gửi biểu mẫu, chúng tôi sẽ thêm việc cần làm mới tạo vào danh sách todo.

Hãy xem cách triển khai cấp cao này của chức năng này trong 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);
      });
  }
}

Chúng ta khai báo một hàm dựng trong đó chúng ta chèn lớp MatDialog. Bên trong newTask, chúng tôi:

  • Mở hộp thoại mới bằng cách sử dụng TaskDialogComponent mà chúng tôi sẽ xác định sau một chút.
  • Chỉ định rằng chúng ta muốn hộp thoại có chiều rộng là 270px.
  • Chuyển một tác vụ trống vào hộp thoại dưới dạng dữ liệu. Trong TaskDialogComponent, chúng tôi sẽ có thể tham chiếu đến đối tượng dữ liệu này.
  • Chúng ta đăng ký sự kiện đóng và thêm việc cần làm từ đối tượng result vào mảng todo.

Để đảm bảo quá trình này hoạt động, trước tiên, chúng tôi cần nhập MatDialogModule vào AppModule:

src/app/app.module.ts

...
import { MatDialogModule } from '@angular/material/dialog';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    ...
    MatDialogModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Bây giờ, hãy tạo TaskDialogComponent. Chuyển đến thư mục src/app và chạy:

ng generate component task-dialog

Để triển khai chức năng của ứng dụng này, trước tiên hãy mở: src/app/task-dialog/task-dialog.component.html và thay thế nội dung của ứng dụng này bằng:

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>

Trong mẫu ở trên, chúng ta tạo một biểu mẫu có hai trường cho titledescription. Chúng ta dùng lệnh cdkFocusInput để tự động đặt tiêu điểm vào title khi người dùng mở hộp thoại.

Hãy lưu ý cách bên trong mẫu mà chúng tôi tham chiếu đến thuộc tính data của thành phần. Đây cũng sẽ là data mà chúng tôi chuyển đến phương thức open của dialog trong AppComponent. Để cập nhật tiêu đề và nội dung mô tả việc cần làm khi người dùng thay đổi nội dung của các trường tương ứng, chúng ta sẽ dùng liên kết dữ liệu hai chiều với ngModel.

Khi người dùng nhấp vào nút OK, chúng tôi sẽ tự động trả về kết quả { task: data.task }, đây là nhiệm vụ mà chúng tôi đã thay đổi bằng cách sử dụng các trường biểu mẫu trong mẫu ở trên.

Bây giờ, hãy triển khai bộ điều khiển của thành phần:

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);
  }
}

Trong TaskDialogComponent, chúng ta chèn một tham chiếu đến hộp thoại để có thể đóng chế độ đó, cũng như chèn giá trị của nhà cung cấp được liên kết với mã thông báo MAT_DIALOG_DATA. Đây là đối tượng dữ liệu mà chúng ta đã chuyển vào phương thức mở trong AppComponent ở trên. Chúng tôi cũng khai báo thuộc tính riêng tư backupTask, là bản sao của tác vụ mà chúng ta đã chuyển cùng với đối tượng dữ liệu.

Khi người dùng nhấn nút hủy, chúng tôi sẽ khôi phục các thuộc tính có thể đã thay đổi của this.data.task và đóng hộp thoại, chuyển kết quả là this.data.

Có 2 loại mà chúng tôi đã tham chiếu nhưng chưa khai báo – TaskDialogDataTaskDialogResult. Bên trong src/app/task-dialog/task-dialog.component.ts, hãy thêm các khai báo sau vào cuối tệp:

src/app/task-dialog/task-dialog.component.ts

...
export interface TaskDialogData {
  task: Partial<Task>;
  enableDelete: boolean;
}

export interface TaskDialogResult {
  task: Task;
  delete?: boolean;
}

Điều cuối cùng chúng ta cần làm trước khi chuẩn bị chức năng là nhập một vài mô-đun vào 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 { }

Khi nhấp vào nút "Add Tasks" ngay bây giờ, bạn sẽ thấy giao diện người dùng sau:

33bcb987fade2a87.png

7. Cải thiện kiểu của ứng dụng

Để ứng dụng trở nên hấp dẫn hơn, chúng tôi sẽ cải thiện bố cục bằng cách điều chỉnh kiểu tệp một chút. Chúng tôi muốn đặt các làn nước bên cạnh nhau. Chúng tôi cũng muốn một số điều chỉnh nhỏ đối với nút "Add Tasks" và nhãn danh sách trống.

Mở src/app/app.component.css và thêm các kiểu sau vào phía dưới cùng:

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

Trong đoạn mã ở trên, chúng ta sẽ điều chỉnh bố cục của thanh công cụ và nhãn của thanh công cụ. Chúng tôi cũng đảm bảo rằng nội dung được căn chỉnh theo chiều ngang bằng cách đặt chiều rộng thành 1400px và lề của nội dung thành auto. Tiếp theo, bằng cách sử dụng hộp linh hoạt, chúng tôi đặt các đường bơi cạnh nhau, và cuối cùng là điều chỉnh cách chúng tôi hình ảnh hóa các việc cần làm và danh sách trống.

Sau khi ứng dụng của bạn tải lại, bạn sẽ thấy giao diện người dùng sau:

69225f0b1aa5cb50.png

Mặc dù chúng tôi đã cải thiện đáng kể các kiểu của ứng dụng, nhưng chúng tôi vẫn gặp phải một vấn đề khó chịu khi di chuyển các tác vụ:

f9aae712027624af.png

Khi chúng ta bắt đầu kéo "Mua sữa" nhiệm vụ, chúng ta thấy hai thẻ cho cùng một nhiệm vụ - một thẻ mà chúng ta đang kéo và thẻ trong làn bơi. Angular CDK cung cấp cho chúng ta những tên lớp CSS mà chúng ta có thể dùng để khắc phục vấn đề này.

Thêm các tùy chọn ghi đè kiểu sau vào cuối src/app/app.component.css:

src/app/app.component.css

.cdk-drag-animating {
  transition: transform 250ms;
}

.cdk-drag-placeholder {
  opacity: 0;
}

Trong khi chúng ta kéo một phần tử, Angular CDK\39; sẽ sao chép và thả bản sao đó vào vị trí mà chúng ta sẽ thả bản gốc. Để đảm bảo phần tử này không hiển thị, chúng ta sẽ đặt thuộc tính độ mờ trong lớp cdk-drag-placeholder, mà CDK sẽ thêm vào phần giữ chỗ.

Ngoài ra, khi chúng ta thả một phần tử, CDK sẽ thêm lớp cdk-drag-animating. Để hiển thị ảnh động mượt mà thay vì gắn trực tiếp phần tử, chúng ta xác định quá trình chuyển đổi có thời lượng là 250ms.

Chúng tôi cũng muốn thực hiện một số điều chỉnh nhỏ về các kiểu công việc của mình. Trong task.component.css, hãy đặt màn hình của phần tử lưu trữ thành block và đặt một số lề:

src/app/task/task.component.css

:host {
  display: block;
}

.item {
  margin-bottom: 10px;
  cursor: pointer;
}

8. Chỉnh sửa và xóa việc cần làm hiện có

Để chỉnh sửa và xóa các việc cần làm hiện có, chúng tôi sẽ sử dụng lại hầu hết chức năng đã triển khai! Khi người dùng nhấp đúp vào một việc cần làm, chúng tôi sẽ mở TaskDialogComponent và điền hai trường trong biểu mẫu vào cùng với titledescription của việc cần làm đó.

Đối với TaskDialogComponent, chúng tôi cũng sẽ thêm nút xóa. Khi người dùng nhấp vào nút đó, chúng tôi sẽ chuyển một hướng dẫn xóa. Thao tác này sẽ kết thúc bằng AppComponent.

Thay đổi duy nhất chúng tôi cần thực hiện trong TaskDialogComponent là trong mẫu của mẫu:

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>

Nút này hiển thị biểu tượng xóa tài liệu. Khi người dùng nhấp vào nút đó, chúng ta sẽ đóng hộp thoại và chuyển kết quả là đối tượng { task: data.task, delete: true }. Ngoài ra, hãy lưu ý rằng chúng tôi đặt nút này làm vòng tròn bằng mat-fab, đặt màu của nút này là chính và chỉ hiển thị nút này khi dữ liệu hộp thoại đã bật tùy chọn xóa.

Phần còn lại của quá trình triển khai chức năng chỉnh sửa và xoá có trong AppComponent. Thay thế phương thức editTask bằng phương thức sau:

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;
      }
    });
  }
  ...
}

Hãy xem các đối số của phương thức editTask:

  • Danh sách loại 'done' | 'todo' | 'inProgress',, là một loại hợp nhất theo chuỗi với các giá trị tương ứng với các thuộc tính được liên kết với từng làn bơi.
  • Việc cần làm hiện tại mà chúng tôi muốn chỉnh sửa.

Trong phần nội dung của phương thức, trước tiên chúng ta mở một bản sao của TaskDialogComponent. Khi data, đối tượng này sẽ chuyển một đối tượng hằng nghĩa. Lệnh này chỉ định việc chúng ta muốn chỉnh sửa, đồng thời bật nút chỉnh sửa trong biểu mẫu bằng cách đặt thuộc tính enableDelete thành true.

Khi nhận được kết quả từ hộp thoại, chúng ta sẽ xử lý hai trường hợp:

  • Khi cờ delete được đặt thành true (tức là khi người dùng đã nhấn nút xóa), chúng tôi sẽ xóa việc cần làm đó khỏi danh sách tương ứng.
  • Ngoài ra, chúng tôi chỉ thay thế nhiệm vụ trên chỉ mục đã cho bằng tác vụ mà chúng tôi nhận được từ kết quả hộp thoại.

9. Tạo dự án Firebase mới

Bây giờ, hãy tạo một dự án Firebase mới!

10. Thêm Firebase vào dự án

Trong phần này, chúng tôi sẽ tích hợp dự án của mình với Firebase! Nhóm Firebase cung cấp gói @angular/fire để cung cấp khả năng tích hợp giữa hai công nghệ. Để thêm hỗ trợ Firebase vào ứng dụng, hãy mở thư mục gốc của không gian làm việc và chạy:

ng add @angular/fire

Lệnh này cài đặt gói @angular/fire và đặt cho bạn một vài câu hỏi. Trong thiết bị thanh toán, bạn sẽ thấy các thông tin như:

9ba88c0d52d18d0.png

Trong thời gian chờ đợi, quá trình cài đặt sẽ mở ra một cửa sổ trình duyệt để bạn có thể xác thực bằng tài khoản Firebase của mình. Cuối cùng, thao tác này yêu cầu bạn chọn một dự án Firebase và tạo một số tệp trên ổ đĩa của bạn.

Tiếp theo, chúng ta cần tạo một cơ sở dữ liệu trên Firestore! Trong "Cloud Firestore" Click "Create Database."

1e4a08b5a2462956.png

Sau đó, tạo cơ sở dữ liệu ở chế độ thử nghiệm:

ac1181b2c32049f9.png

Cuối cùng, hãy chọn một khu vực:

34bb94cc542a0597.png

Điều còn lại bây giờ là thêm cấu hình Firebase vào môi trường của bạn. Bạn có thể tìm thấy cấu hình dự án trong Bảng điều khiển của Firebase.

  • Nhấp vào biểu tượng Bánh răng bên cạnh Tổng quan về dự án.
  • Chọn Settings (Cài đặt dự án).

c8253a20031de8a9.png

Trong "Your apps", chọn "Ứng dụng web":

428a1abcd0f90b23.png

Tiếp theo, hãy đăng ký ứng dụng của bạn và đảm bảo bạn bật "Lưu trữ Firebase":

586e44cb27dd8f39.png

Sau khi bạn nhấp vào "Đăng ký ứng dụng", bạn có thể sao chép cấu hình của mình vào src/environments/environment.ts:

e30f142d79cecf8f.png

Cuối cùng, tệp cấu hình của bạn sẽ có dạng như sau:

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. Chuyển dữ liệu sang Firestore

Bây giờ, chúng tôi đã thiết lập SDK Firebase, hãy dùng @angular/fire để di chuyển dữ liệu của chúng tôi đến Firestore! Trước tiên, hãy nhập những mô-đun mà chúng ta cần trong 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 {}

Vì chúng ta sẽ sử dụng Firestore, nên chúng ta cần đưa AngularFirestore vào hàm dựng của AppComponent\39:

src/app/app.component.ts

...
import { AngularFirestore } from '@angular/fire/firestore';

@Component({...})
export class AppComponent {
  ...
  constructor(private dialog: MatDialog, private store: AngularFirestore) {}
  ...
}

Tiếp theo, chúng ta cập nhật cách khởi chạy các mảng bể bơi:

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[]>;
  ...
}

Ở đây, chúng ta sử dụng AngularFirestore để lấy nội dung của bộ sưu tập ngay từ cơ sở dữ liệu. Lưu ý rằng valueChanges trả về có thể quan sát thay vì một mảng và chúng tôi cũng chỉ định rằng trường mã nhận dạng của các tài liệu trong tập hợp này phải được gọi là id để khớp với tên mà chúng tôi sử dụng trong giao diện Task. Quá trình quan sát được valueChanges trả về phát ra một tập hợp các tác vụ bất cứ khi nào thay đổi.

Vì chúng ta đang làm việc với các đối tượng phát ra dữ liệu thay vì mảng, nên chúng ta cần phải cập nhật cách thêm, xoá và chỉnh sửa việc cần làm, cũng như chức năng để di chuyển việc cần làm giữa các làn đường. Thay vì thay đổi các mảng trong bộ nhớ, chúng ta sẽ dùng SDK Firebase để cập nhật dữ liệu trong cơ sở dữ liệu.

Trước tiên, hãy xem cách sắp xếp lại thứ tự. Thay thế phương thức drop trong src/app/app.component.ts bằng:

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
  );
}

Trong đoạn mã ở trên, mã mới được làm nổi bật. Để di chuyển một nhiệm vụ từ làn bơi hiện tại sang mục tiêu mục tiêu, chúng ta sẽ xóa việc cần làm khỏi bộ sưu tập đầu tiên và thêm việc cần làm đó vào bộ sưu tập thứ hai. Vì chúng ta thực hiện 2 thao tác mà chúng ta muốn trông giống như một thao tác (tức là đặt thao tác ở dạng nguyên tử), nên chúng ta chạy các thao tác này trong giao dịch Firestore.

Tiếp theo, hãy cập nhật phương thức editTask để sử dụng Firestore! Bên trong trình xử lý hộp thoại đóng, chúng ta cần thay đổi các dòng mã sau:

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);
  }
});
...

Chúng ta truy cập vào tài liệu mục tiêu tương ứng với nhiệm vụ mà chúng ta thao tác bằng SDK Firestore và xóa hoặc cập nhật tài liệu đó.

Cuối cùng, chúng ta cần cập nhật phương thức tạo tác vụ mới. Thay thế this.todo.push('task') bằng: this.store.collection('todo').add(result.task).

Xin lưu ý rằng hiện nay, bộ sưu tập của chúng ta không phải là các mảng mà là các đối tượng có thể quan sát. Để có thể trực quan hóa chúng, chúng ta cần cập nhật mẫu của AppComponent. Bạn chỉ cần thay thế mọi quyền truy cập của thuộc tính todo, inProgressdone bằng todo | async, inProgress | asyncdone | async tương ứng.

Dịch vụ không đồng bộ tự động đăng ký đối tượng phát ra dữ liệu được liên kết với bộ sưu tập. Khi các đối tượng phát ra phát ra một giá trị mới, Angular sẽ tự động chạy tính năng phát hiện thay đổi và xử lý mảng đã phát ra.

Ví dụ: hãy xem những thay đổi chúng ta cần thực hiện trong làn đường todo:

src/app/app.component.html

<mat-card
  cdkDropList
  id="todo"
  #todoList="cdkDropList"
  [cdkDropListData]="todo | async"
  [cdkDropListConnectedTo]="[doneList, inProgressList]"
  (cdkDropListDropped)="drop($event)"
  class="list">
  <p class="empty-label" *ngIf="(todo | async)?.length === 0">Empty list</p>
  <app-task (edit)="editTask('todo', $event)" *ngFor="let task of todo | async" cdkDrag [task]="task"></app-task>
</mat-card>

Khi chuyển dữ liệu vào lệnh cdkDropList, chúng ta áp dụng dấu gạch không đồng bộ. Lệnh này tương tự trong lệnh *ngIf, nhưng xin lưu ý rằng chúng ta cũng sử dụng chuỗi tùy chọn (còn gọi là toán tử điều hướng an toàn trong Angular), khi truy cập vào thuộc tính length để đảm bảo chúng ta không gặp lỗi thời gian chạy nếu todo | async không phải là null hoặc undefined.

Bây giờ khi bạn tạo một tác vụ mới trong giao diện người dùng và mở Firestore, bạn sẽ thấy một tác vụ như sau:

dd7ee20c0a10ebe2.png

12. Cải thiện nội dung cập nhật đáng tin cậy

Trong ứng dụng, chúng tôi hiện đang thực hiện cập nhật tối ưu. Chúng tôi có nguồn dữ liệu chính xác trong Firestore, nhưng đồng thời có các bản sao cục bộ của các tác vụ; khi có thể quan sát được liên kết với các bộ sưu tập, chúng tôi nhận được một loạt nhiệm vụ. Khi một hành động của người dùng làm thay đổi trạng thái, trước tiên, chúng tôi sẽ cập nhật các giá trị cục bộ rồi áp dụng thay đổi đó cho Firestore.

Khi di chuyển một nhiệm vụ từ bể bơi này sang đường bơi khác, chúng ta sẽ gọi transferArrayItem,. Thao tác này hoạt động trên các bản sao cục bộ của các mảng đại diện cho các nhiệm vụ trong mỗi đường bơi. SDK Firebase coi những mảng này là không thể thay đổi, có nghĩa là vào lần tới Angular chạy tính năng phát hiện thay đổi, chúng tôi sẽ nhận được các bản sao mới của chúng, sẽ hiển thị trạng thái trước đó trước khi chúng tôi chuyển tác vụ.

Đồng thời, chúng tôi kích hoạt bản cập nhật Firestore và SDK Firebase sẽ kích hoạt bản cập nhật có giá trị chính xác, vì vậy, chỉ trong vài mili giây, giao diện người dùng sẽ đạt được trạng thái chính xác. Thao tác này giúp tác vụ mà chúng ta vừa chuyển được từ danh sách đầu tiên sang danh sách tiếp theo. Bạn có thể xem điều này rõ ràng trên ảnh GIF dưới đây:

70b946eebfa6f316.gif

Cách phù hợp để giải quyết vấn đề này sẽ khác nhau tùy theo ứng dụng, nhưng trong mọi trường hợp, chúng tôi cần đảm bảo rằng chúng tôi duy trì trạng thái nhất quán cho đến khi dữ liệu của chúng tôi cập nhật.

Chúng ta có thể tận dụng BehaviorSubject để bao gồm đối tượng tiếp nhận dữ liệu ban đầu mà chúng ta nhận được từ valueChanges. Về cơ bản, BehaviorSubject giữ một mảng có thể thay đổi liên tục cập nhật từ transferArrayItem.

Để triển khai bản sửa lỗi, chúng ta chỉ cần cập nhật 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[]>;
...
}

Tất cả những gì chúng ta làm trong đoạn mã ở trên là tạo BehaviorSubject, phát ra giá trị mỗi khi có thể quan sát được liên kết với các thay đổi của bộ sưu tập.

Mọi thứ hoạt động như dự kiến vì BehaviorSubject sử dụng lại mảng trên các lệnh gọi phát hiện thay đổi và chỉ cập nhật khi chúng tôi nhận được giá trị mới từ Firestore.

13. Triển khai ứng dụng

Tất cả những gì chúng ta cần làm để triển khai ứng dụng của mình là chạy:

ng deploy

Lệnh này sẽ:

  1. Xây dựng ứng dụng bằng cấu hình sản xuất của ứng dụng để áp dụng tối ưu hóa thời gian biên dịch.
  2. Triển khai ứng dụng của bạn sang Dịch vụ lưu trữ Firebase.
  3. Đầu ra URL để bạn có thể xem trước kết quả.

14. Xin chúc mừng

Xin chúc mừng, bạn!

Bạn đã tạo một giao diện người dùng, trong đó có ba cột thể hiện trạng thái của những việc cần làm khác nhau. Khi sử dụng CDK Angular, bạn đã triển khai tính năng kéo và thả các nhiệm vụ trên các cột. Sau đó, bằng cách sử dụng tài liệu Angular, bạn đã tạo một biểu mẫu để tạo những việc mới và chỉnh sửa những việc hiện có. Tiếp theo, bạn đã tìm hiểu cách sử dụng @angular/fire và chuyển tất cả trạng thái của ứng dụng sang Firestore. Cuối cùng, bạn đã triển khai ứng dụng của mình lên Lưu trữ Firebase.

Tiếp theo là gì?

Hãy nhớ rằng chúng tôi đã triển khai ứng dụng này bằng cách sử dụng cấu hình thử nghiệm. Trước khi triển khai phiên bản chính thức của ứng dụng, hãy đảm bảo bạn đã thiết lập đúng quyền. Bạn có thể tìm hiểu cách thực hiện việc này tại đây.

Hiện tại, chúng tôi không giữ nguyên thứ tự các nhiệm vụ trong một làn bơi cụ thể. Để triển khai tính năng này, bạn có thể sử dụng trường đơn đặt hàng trong tài liệu nhiệm vụ và sắp xếp dựa trên trường đó.

Ngoài ra, chúng tôi đã xây dựng bảng kanban chỉ cho một người dùng, nghĩa là chúng tôi có một bảng kanban cho bất kỳ ai mở ứng dụng. Để triển khai các bảng riêng cho những người dùng ứng dụng khác nhau, bạn sẽ cần thay đổi cấu trúc cơ sở dữ liệu của mình. Tìm hiểu các phương pháp hay nhất của Firestoretại đây.