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.
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:
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/material
là Bộ 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:
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
, title
và description
(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:
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ệucdkDropListConnectedTo
– tham chiếu đến cáccdkDropList
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ứceditTask
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áchtodo
như đã làm ở trên và chuyển thông tin đầu vàotask
. Tuy nhiên, lần này chúng ta cũng thêm lệnhcdkDrag
. 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 inProgress
và done
, cùng với phương thức editTask
và 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
);
}
}
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à:
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ảngtodo
.
Để đả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 title
và description
. 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 – TaskDialogData
và TaskDialogResult
. 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:
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:
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ụ:
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 title
và description
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ànhtrue
(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!
- Chuyển đến Bảng điều khiển của Firebase.
- Tạo một dự án mới có tên "KanbanFire"
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ư:
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."
Sau đó, tạo cơ sở dữ liệu ở chế độ thử nghiệm:
Cuối cùng, hãy chọn một khu vực:
Đ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).
Trong "Your apps", chọn "Ứng dụng web":
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":
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
:
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
, inProgress
và done
bằng todo | async
, inProgress | async
và done | 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:
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:
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ẽ:
- 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.
- Triển khai ứng dụng của bạn sang Dịch vụ lưu trữ Firebase.
- Đầ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.