1. บทนำ
อัปเดตล่าสุด: 19-09-2020
สิ่งที่คุณจะสร้าง
ใน Codelab นี้ เราจะสร้างเว็บบอร์ด Kanban ด้วย Angular และ Firebase แอปขั้นสุดท้ายจะมีงาน 3 หมวดหมู่ ได้แก่ งานที่ยังทําไม่เสร็จ กําลังดําเนินการ และเสร็จสมบูรณ์แล้ว เราจะสร้าง ลบงาน และโอนจากหมวดหมู่หนึ่งไปยังอีกหมวดหมู่หนึ่งได้โดยใช้การลากและวาง
เราจะพัฒนาอินเทอร์เฟซผู้ใช้โดยใช้ Angular และใช้ Firestore เป็นร้านค้าถาวรของเรา ในตอนท้ายของ Codelab เราจะทําให้แอปใช้งานได้กับโฮสติ้งของ Firebase โดยใช้ Angular CLI
สิ่งที่จะได้เรียนรู้
- วิธีใช้วัสดุ Angular และ CDK
- วิธีเพิ่มการผสานรวม Firebase ลงในแอป Angular
- วิธีเก็บข้อมูลถาวรไว้ใน Firestore
- วิธีทําให้แอปใช้งานได้บนโฮสติ้งของ Firebase โดยใช้ Angular CLI ด้วยคําสั่งเดียว
สิ่งที่ต้องมี
Codelab นี้จะถือว่าคุณมีบัญชี 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 จะสร้างโครงสร้างโปรเจ็กต์และติดตั้งทรัพยากร Dependency ทั้งหมด เมื่อกระบวนการติดตั้งเสร็จสมบูรณ์แล้ว ให้ไปที่ไดเรกทอรี kanban-fire
และเริ่มเซิร์ฟเวอร์การพัฒนา Angular CLI' ดังนี้
ng serve
เปิด http://localhost:4200 และคุณจะเห็นผลลัพธ์ในลักษณะนี้
เปิด src/app/app.component.html
ในเครื่องมือแก้ไข และลบเนื้อหาทั้งหมดออก เมื่อกลับไปที่ http://localhost:4200 คุณควรจะเห็นหน้าว่าง
3. การเพิ่ม Material และ CDK
Angular มาพร้อมกับการใช้คอมโพเนนต์อินเทอร์เฟซผู้ใช้ที่เป็นไปตามข้อกําหนดของการออกแบบ Material เป็นส่วนหนึ่งของแพ็กเกจ @angular/material
หนึ่งในทรัพยากร Dependency ของ @angular/material
คือ ชุดพัฒนาคอมโพเนนต์หรือ CDK CDK มีเครื่องมือพื้นฐาน เช่น ยูทิลิตี a11y การลากและวาง และการวางซ้อน เราแจกจ่าย CDK ในแพ็กเกจ @angular/cdk
วิธีเพิ่มเนื้อหาลงในแอป
ng add @angular/material
คําสั่งนี้จะให้คุณเลือกธีม หากคุณต้องการใช้รูปแบบการพิมพ์ของวัสดุส่วนกลางและหากคุณต้องการตั้งค่าภาพเคลื่อนไหวของเบราว์เซอร์สําหรับ Angular Material เลือก "Indigo/Pink" เพื่อให้ได้ผลลัพธ์เช่นเดียวกับใน Codelab นี้ และตอบว่า "Yes" สําหรับคําถามสองข้อสุดท้าย
คําสั่ง 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 จึงต้องนําเข้าโมดูลที่เกี่ยวข้องใน AppModule
คุณจะเห็นสิ่งต่อไปนี้ในหน้าจอ
ไม่เลวเลยด้วย HTML เพียง 4 บรรทัดและการนําเข้า 2 รายการ
4. การแสดงภาพ
ในขั้นตอนถัดไป มาสร้างคอมโพเนนต์ที่เราใช้เพื่อแสดงภาพงานใน Kanban Board กัน
ไปที่ไดเรกทอรี src/app
และเรียกใช้คําสั่ง CLI ต่อไปนี้
ng generate component task
คําสั่งนี้จะสร้าง TaskComponent
และเพิ่มการประกาศไปยัง AppModule
สร้างไฟล์ชื่อ task.ts
ภายในไดเรกทอรี task
เราจะใช้ไฟล์นี้เพื่อกําหนดอินเทอร์เฟซของงานใน 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 { }
ต่อไปเราจะสร้างงาน 2-3 อย่างใน AppComponent
และแสดงภาพโดยใช้ TaskComponent
ใน AppComponent
ให้กําหนดอาร์เรย์ชื่อ todo
และภายในอาร์เรย์จะเพิ่มงาน 2 รายการดังนี้
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!'
}
];
}
ตอนนี้ให้เพิ่มคําสั่ง *ngFor
ไว้ที่ด้านล่างของapp.component.html
src/app/app.component.html
<app-task *ngFor="let task of todo" [task]="task"></app-task>
สิ่งที่จะเกิดขึ้นเมื่อเปิดเบราว์เซอร์มีดังนี้
5. การใช้การลากและวางสําหรับงาน
เราพร้อมสําหรับส่วนที่สนุกแล้วตอนนี้! มาสร้างช่องทางว่ายน้ํา 3 ช่องทางสําหรับ 3 สถานะใน 3 สถานะเหล่านั้นกัน แล้วใช้ฟังก์ชัน 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
ที่ครอบคลุมทั้ง 3 เลน โดยใช้ชื่อคลาส "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>
...
ขั้นแรก เราให้กําหนดช่องว่ายน้ําเป็น mat-card
ซึ่งใช้คําสั่ง cdkDropList
เราใช้ mat-card
เนื่องจากสไตล์ของคอมโพเนนต์นี้ cdkDropList
จะช่วยให้เราวางงานลงในองค์ประกอบได้ในภายหลัง นอกจากนี้ เรายังตั้งค่าอินพุต 2 รายการต่อไปนี้
cdkDropListData
- อินพุตของรายการแบบเลื่อนลงที่ช่วยให้เราระบุอาร์เรย์ข้อมูลได้cdkDropListConnectedTo
- การอ้างอิงไปยังcdkDropList
อื่นๆ ที่cdkDropList
ปัจจุบันเชื่อมต่ออยู่ การตั้งค่าอินพุตนี้เราจะระบุรายการอื่นๆ ที่นําไปวางได้
นอกจากนี้ เราต้องการจัดการเหตุการณ์แบบเลื่อนลงโดยใช้เอาต์พุต cdkDropListDropped
เมื่อ cdkDropList
ปล่อยเอาต์พุตนี้ เราจะเรียกใช้เมธอด drop
ที่ประกาศภายใน AppComponent
และส่งเหตุการณ์ปัจจุบันเป็นอาร์กิวเมนต์
โปรดสังเกตว่าเรายังระบุ id
เพื่อใช้เป็นตัวระบุสําหรับคอนเทนเนอร์นี้ และชื่อ class
ด้วยเพื่อให้เราสามารถจัดรูปแบบได้ คราวนี้มาดูเนื้อหาสําหรับเด็กของ mat-card
กัน องค์ประกอบ 2 อย่างที่เรามี ได้แก่
- ย่อหน้าที่เราใช้เพื่อแสดงข้อความ "ว่างในรายการและโควต้า; เมื่อไม่มีรายการในรายการ
todo
- คอมโพเนนต์
app-task
โปรดสังเกตว่าเราจะจัดการเอาต์พุตedit
ที่ประกาศไว้ในตอนแรกด้วยการเรียกใช้เมธอดeditTask
ด้วยชื่อรายการและออบเจ็กต์$event
วิธีนี้จะช่วยเราแทนที่งานที่แก้ไขแล้วจากรายการที่ถูกต้อง ในลําดับต่อไป เราจะปรับปรุงรายการtodo
ตามที่ได้ทําไปข้างต้นและส่งอินพุตtask
แต่ในครั้งนี้ เราจะเพิ่มคําสั่งcdkDrag
ด้วย เพื่อให้ลากงานแต่ละอย่างได้
เพื่อให้การดําเนินการนี้เสร็จสมบูรณ์ เราต้องอัปเดต app.module.ts
และรวมการนําเข้าไปยัง 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
เราจะตรวจสอบว่าเราวางรายการเดียวกันกับงานที่มาส่งก่อนหรือไม่ หากเป็นเช่นนั้น เราจะส่งคืนทันที ไม่เช่นนั้นเราจะโอนงานปัจจุบันไปที่จุดว่ายน้ําปลายทาง
ผลลัพธ์ที่ได้ควรเป็นดังนี้
ณ จุดนี้คุณน่าจะโอนรายการระหว่าง 2 รายการนี้ได้
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
และเพิ่มปุ่มด้วย &&tt;add
" ไอคอนสื่อการเรียนการสอนด้านข้างป้ายกํากับ "เพิ่มงาน&&tt; เราต้องใช้ Wrapper เพิ่มเติมเพื่อวางตําแหน่งปุ่มที่ด้านบนของรายการว่ายน้ํา ซึ่งเราจะวางไว้ข้างๆ กันโดยใช้ Flexbox เนื่องจากปุ่มนี้ใช้คอมโพเนนต์ปุ่มวัสดุ เราจึงต้องนําเข้าโมดูลที่เกี่ยวข้องใน AppModule
ดังนี้
src/app/app.module.ts
...
import { MatButtonModule } from '@angular/material/button';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
MatButtonModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
ต่อไปให้ใช้ฟังก์ชันสําหรับเพิ่มงานใน AppComponent
กัน เราจะใช้กล่องโต้ตอบที่เป็นรูปธรรม ในกล่องโต้ตอบ เราจะมีแบบฟอร์มที่มี 2 ช่อง ได้แก่ ชื่อและคําอธิบาย เมื่อผู้ใช้คลิกปุ่ม"เพิ่มงาน" เราจะเปิดกล่องโต้ตอบ และเมื่อผู้ใช้ส่งแบบฟอร์ม เราจะเพิ่มงานที่สร้างใหม่ลงในรายการtodo
มาดูการใช้งานระดับสูงของฟังก์ชันนี้ใน AppComponent
กัน
src/app/app.component.ts
...
import { MatDialog } from '@angular/material/dialog';
@Component(...)
export class AppComponent {
...
constructor(private dialog: MatDialog) {}
newTask(): void {
const dialogRef = this.dialog.open(TaskDialogComponent, {
width: '270px',
data: {
task: {},
},
});
dialogRef
.afterClosed()
.subscribe((result: TaskDialogResult|undefined) => {
if (!result) {
return;
}
this.todo.push(result.task);
});
}
}
เราประกาศเครื่องมือสร้างที่เราแทรกคลาส MatDialog
ภายใน newTask
เราจะดําเนินการดังนี้
- เปิดกล่องโต้ตอบใหม่โดยใช้
TaskDialogComponent
ซึ่งเราจะอธิบายในอีกสักครู่ - ระบุว่าต้องการให้กล่องโต้ตอบมีความกว้าง
270px.
- ส่งผ่านงานที่ว่างเปล่าไปยังกล่องโต้ตอบเป็นข้อมูล ในอีก
TaskDialogComponent
เราจะได้รับการอ้างอิงไปยังออบเจ็กต์ข้อมูลนี้ - เราติดตามเหตุการณ์ปิดและเพิ่มงานจากออบเจ็กต์
result
ไปยังอาร์เรย์todo
เพื่อให้ทํางานได้ เราจะต้องนําเข้า MatDialogModule
ใน AppModule
ก่อน:
src/app/app.module.ts
...
import { MatDialogModule } from '@angular/material/dialog';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
MatDialogModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
ต่อไปเราจะมาสร้าง TaskDialogComponent
ไปที่ไดเรกทอรี src/app
แล้วเรียกใช้
ng generate component task-dialog
หากต้องการใช้ฟังก์ชันการทํางาน ให้เปิด: src/app/task-dialog/task-dialog.component.html
แล้วแทนที่เนื้อหาด้วยสิ่งต่อไปนี้
src/app/task-dialog/task-dialog.component.html
<mat-form-field>
<mat-label>Title</mat-label>
<input matInput cdkFocusInitial [(ngModel)]="data.task.title" />
</mat-form-field>
<mat-form-field>
<mat-label>Description</mat-label>
<textarea matInput [(ngModel)]="data.task.description"></textarea>
</mat-form-field>
<div mat-dialog-actions>
<button mat-button [mat-dialog-close]="{ task: data.task }">OK</button>
<button mat-button (click)="cancel()">Cancel</button>
</div>
ในเทมเพลตด้านบน เราจะสร้างแบบฟอร์มที่มี 2 ช่องสําหรับ title
และ description
เราใช้คําสั่ง cdkFocusInput
เพื่อโฟกัสอินพุต title
โดยอัตโนมัติเมื่อผู้ใช้เปิดกล่องโต้ตอบ
โปรดสังเกตว่าภายในเทมเพลตที่เราอ้างอิงพร็อพเพอร์ตี้ data
ของคอมโพเนนต์ ซึ่งเป็นdata
เดียวกับที่ส่งต่อไปยังเมธอด open
ของdialog
ในAppComponent
หากต้องการอัปเดตชื่อและคําอธิบายของงานเมื่อผู้ใช้เปลี่ยนเนื้อหาของช่องที่เกี่ยวข้อง เราจะใช้การเชื่อมโยงข้อมูลแบบ 2 ทางกับ ngModel
เมื่อผู้ใช้คลิกปุ่มตกลง เราจะแสดงผล { 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
มีข้อมูลอยู่ 2 ประเภทที่เราอ้างอิงและยังไม่ได้ประกาศ คือ TaskDialogData
และ TaskDialogResult
ใน src/app/task-dialog/task-dialog.component.ts
ให้เพิ่มการประกาศต่อไปนี้ที่ด้านล่างของไฟล์
src/app/task-dialog/task-dialog.component.ts
...
export interface TaskDialogData {
task: Partial<Task>;
enableDelete: boolean;
}
export interface TaskDialogResult {
task: Task;
delete?: boolean;
}
สิ่งสุดท้ายที่ต้องทําก่อนเตรียมฟังก์ชันให้พร้อมคือการนําเข้าโมดูลบางรายการใน AppModule
src/app/app.module.ts
...
import { MatInputModule } from '@angular/material/input';
import { FormsModule } from '@angular/forms';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
MatInputModule,
FormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
เมื่อคลิกปุ่ม "เพิ่มงาน&โควต้า; ตอนนี้ คุณควรจะเห็นอินเทอร์เฟซผู้ใช้ต่อไปนี้:
7. การปรับปรุงรูปแบบของแอป
เพื่อให้แอปพลิเคชันดูน่าสนใจมากขึ้น เราจะปรับปรุงเลย์เอาต์ของแอปพลิเคชันโดยปรับแต่งรูปแบบเล็กน้อย เราอยากวางตําแหน่งจุดลงตัวให้อยู่ใกล้กัน นอกจากนี้เราต้องการการปรับเปลี่ยนเล็กน้อยของปุ่ม "เพิ่มงาน" และป้ายกํากับรายการที่ว่างเปล่า
เปิด src/app/app.component.css
และเพิ่มสไตล์ต่อไปนี้ที่ด้านล่าง
src/app/app.component.css
mat-toolbar {
margin-bottom: 20px;
}
mat-toolbar > span {
margin-left: 10px;
}
.content-wrapper {
max-width: 1400px;
margin: auto;
}
.container-wrapper {
display: flex;
justify-content: space-around;
}
.container {
width: 400px;
margin: 0 25px 25px 0;
}
.list {
border: solid 1px #ccc;
min-height: 60px;
border-radius: 4px;
}
app-new-task {
margin-bottom: 30px;
}
.empty-label {
font-size: 2em;
padding-top: 10px;
text-align: center;
opacity: 0.2;
}
ในข้อมูลโค้ดข้างต้น เราจะปรับเลย์เอาต์ของแถบเครื่องมือและป้ายกํากับ นอกจากนี้เรายังทําให้เนื้อหาสอดคล้องกับแนวนอนโดยตั้งค่าความกว้างเป็น 1400px
และระยะขอบเป็น auto
ถัดไปเราใช้ Flexbox วางช่องทําเครื่องหมายถัดจากกัน และสุดท้ายก็ปรับวิธีการแสดงภาพงานและรายการที่ว่างเปล่า
เมื่อแอปโหลดซ้ําแล้ว คุณจะเห็นอินเทอร์เฟซผู้ใช้ต่อไปนี้
แม้ว่าเราจะปรับปรุงสไตล์ของแอปได้อย่างมากแล้ว แต่ก็ยังมีปัญหาที่น่ารําคาญเมื่อต้องย้ายงาน
เมื่อเราเริ่มลาก "Buy milk" เราจะเห็นการ์ด 2 ใบสําหรับงานเดียวกัน คือการ์ดที่เราลาก และรูปที่อยู่ในแถบว่ายน้ํา Angular CDK จะให้ชื่อคลาส CSS ที่เราใช้แก้ไขปัญหานี้ได้
เพิ่มการลบล้างรูปแบบต่อไปนี้ที่ด้านล่างของ src/app/app.component.css
src/app/app.component.css
.cdk-drag-animating {
transition: transform 250ms;
}
.cdk-drag-placeholder {
opacity: 0;
}
เมื่อเราลากองค์ประกอบ CDK' แบบ Angular จะลากและวางโคลน แล้วแทรกลงในตําแหน่งที่เราจะวางต้นฉบับ เพื่อให้แน่ใจว่าองค์ประกอบนี้มองไม่เห็น เราจะตั้งค่าคุณสมบัติความทึบแสงใน cdk-drag-placeholder
คลาส ซึ่ง CDK จะเพิ่มไปยังตัวยึดตําแหน่ง
นอกจากนี้เมื่อวางองค์ประกอบ CDK ก็จะเพิ่มคลาส cdk-drag-animating
เรากําหนดการเปลี่ยนด้วยระยะเวลา 250ms
เพื่อแสดงภาพเคลื่อนไหวที่ราบรื่นแทนการสแนปองค์ประกอบโดยตรง
เราอยากปรับเปลี่ยนสไตล์การทํางานเล็กน้อย ใน task.component.css
ให้องค์ประกอบการแสดงโฮสต์เป็น #block
3 และตั้งค่าระยะขอบ:
src/app/task/task.component.css
:host {
display: block;
}
.item {
margin-bottom: 10px;
cursor: pointer;
}
8. การแก้ไขและลบงานที่มีอยู่
หากต้องการแก้ไขและนํางานที่มีอยู่ออก เราจะใช้ฟังก์ชันส่วนใหญ่ที่นํามาใช้แล้วซ้ําได้ เมื่อผู้ใช้ดับเบิลคลิกที่งาน เราจะเปิด TaskDialogComponent
แล้วป้อนข้อมูลในช่องทั้ง 2 ช่องในแบบฟอร์มว่า 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
เมื่อเราได้รับผลลัพธ์จากกล่องโต้ตอบ เราจะจัดการ 2 สถานการณ์ต่อไปนี้
- เมื่อติดธง
delete
เป็นtrue
(เช่น เมื่อผู้ใช้กดปุ่มลบ) เราจะนํางานออกจากรายการที่เกี่ยวข้อง - หรือจะแทนที่งานในดัชนีที่ระบุด้วยงานที่ได้รับจากผลลัพธ์กล่องโต้ตอบ
9. การสร้างโปรเจ็กต์ Firebase ใหม่
คราวนี้มาสร้างโปรเจ็กต์ Firebase ใหม่กัน
- ไปที่ Firebase Console
- สร้างโปรเจ็กต์ใหม่ด้วยชื่อ "KanbanFire"
10. กําลังเพิ่ม Firebase ไปยังโปรเจ็กต์
ในส่วนนี้เราจะผสานรวมโปรเจ็กต์กับ Firebase ทีม Firebase มีแพ็กเกจ @angular/fire
ที่ผสานรวมทั้ง 2 เทคโนโลยีเข้าด้วยกัน หากต้องการเพิ่มการรองรับ Firebase ลงในแอป ให้เปิดไดเรกทอรีรากของพื้นที่ทํางานและเรียกใช้
ng add @angular/fire
คําสั่งนี้จะติดตั้งแพ็กเกจ @angular/fire
และถามคําถามคุณ 2-3 ข้อ คุณจะเห็นสิ่งต่อไปนี้ในเทอร์มินัล
ในระหว่างนี้ การติดตั้งจะเปิดหน้าต่างเบราว์เซอร์เพื่อให้คุณตรวจสอบสิทธิ์ด้วยบัญชี Firebase ได้ สุดท้ายนี้ ระบบจะให้คุณเลือกโปรเจ็กต์ Firebase และสร้างไฟล์บางรายการในดิสก์
ขั้นต่อไป เราต้องสร้างฐานข้อมูล Firestore &&tt;Cloud Firestore" คลิก&โควต้า;สร้างฐานข้อมูล"
หลังจากนั้น ให้สร้างฐานข้อมูลในโหมดทดสอบ
สุดท้ายให้เลือกภูมิภาค:
สิ่งเดียวที่เหลือก็คือการเพิ่มการกําหนดค่า Firebase ให้กับสภาพแวดล้อมของคุณ คุณดูการกําหนดค่าโปรเจ็กต์ได้ในคอนโซล Firebase
- คลิกไอคอนรูปเฟืองถัดจากภาพรวมโปรเจ็กต์
- เลือกการตั้งค่าโปรเจ็กต์
&"แอปของคุณ" เลือก"เว็บแอป":
ถัดไป ให้ลงทะเบียนแอปพลิเคชันและตรวจสอบว่าได้เปิดใช้ Firebase Hosting "
หลังจากคลิก &เสนอราคา ลงทะเบียนแอปแล้ว คุณจะคัดลอกการกําหนดค่าไปยัง src/environments/environment.ts
ได้โดยทําดังนี้
ในตอนท้าย ไฟล์การกําหนดค่าของคุณควรมีลักษณะดังนี้
src/environments/environment.ts
export const environment = {
production: false,
firebase: {
apiKey: '<your-key>',
authDomain: '<your-project-authdomain>',
databaseURL: '<your-database-URL>',
projectId: '<your-project-id>',
storageBucket: '<your-storage-bucket>',
messagingSenderId: '<your-messaging-sender-id>'
}
};
11. การย้ายข้อมูลไปยัง Firestore
ตอนนี้เราได้ตั้งค่า Firebase SDK แล้ว มาดู @angular/fire
เพื่อย้ายข้อมูลของเราไปยัง Firestore กันเถอะ ขั้นแรก เราให้นําเข้าโมดูลที่จําเป็นใน AppModule
:
src/app/app.module.ts
...
import { environment } from 'src/environments/environment';
import { AngularFireModule } from '@angular/fire';
import { AngularFirestoreModule } from '@angular/fire/firestore';
@NgModule({
declarations: [AppComponent, TaskDialogComponent, TaskComponent],
imports: [
...
AngularFireModule.initializeApp(environment.firebase),
AngularFirestoreModule
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
เนื่องจากเราจะใช้ Firestore เราจึงต้องแทรก AngularFirestore
ในตัวสร้าง AppComponent
':
src/app/app.component.ts
...
import { AngularFirestore } from '@angular/fire/firestore';
@Component({...})
export class AppComponent {
...
constructor(private dialog: MatDialog, private store: AngularFirestore) {}
...
}
ขั้นต่อไป เราจะอัปเดตวิธีเริ่มต้นใช้งานอาร์เรย์ Swว่ายน้ํา ดังนี้
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
เพื่อให้ตรงกับชื่อที่เราใช้ในอินเทอร์เฟซ Task
ค่าที่สังเกตได้ซึ่งแสดงผลโดย valueChanges
จะปล่อยคอลเล็กชันงานทุกครั้งที่มีการเปลี่ยนแปลง
เนื่องจากเรากําลังทํางานกับสิ่งที่สังเกตได้แทนอาร์เรย์ เราจึงต้องอัปเดตวิธีที่เราเพิ่ม ลบ และแก้ไขงาน รวมถึงฟังก์ชันในการย้ายงานระหว่างช่องทางว่ายน้ํา เราจะใช้ SDK ของ Firebase เพื่ออัปเดตข้อมูลในฐานข้อมูลแทนการแปลงอาร์เรย์ในหน่วยความจํา
อันดับแรก เราจะมาดูการจัดเรียงใหม่กัน แทนที่เมธอด 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
);
}
ในข้อมูลโค้ดข้างต้นที่มีการไฮไลต์รหัสใหม่ ในการย้ายงานจากว่ายน้ําปัจจุบันไปยังเป้าหมาย เราจะนํางานออกจากคอลเล็กชันแรกและเพิ่มไปยังคอลเล็กชันที่ 2 เนื่องจากเราทํา 2 การดําเนินการที่เราต้องการมีลักษณะเหมือนกัน (เช่น ทําให้การทํางานเป็นอะตอมเดี่ยว) เราจึงดําเนินการในธุรกรรม Firestore
ถัดไป ให้อัปเดตเมธอด editTask
เพื่อใช้ Firestore ภายในเครื่องจัดการกล่องโต้ตอบแบบปิด เราต้องเปลี่ยนโค้ดบรรทัดต่อไปนี้
src/app/app.component.ts
...
dialogRef.afterClosed().subscribe((result: TaskDialogResult|undefined) => {
if (!result) {
return;
}
if (result.delete) {
this.store.collection(list).doc(task.id).delete();
} else {
this.store.collection(list).doc(task.id).update(task);
}
});
...
เราเข้าถึงเอกสารเป้าหมายที่สอดคล้องกับงานที่เราจัดการโดยใช้ Firestore SDK และลบหรืออัปเดตเอกสาร
สุดท้าย เราต้องอัปเดตวิธีการสร้างงานใหม่ แทนที่ this.todo.push('task')
ด้วย this.store.collection('todo').add(result.task)
โปรดสังเกตว่าตอนนี้คอลเล็กชันของเราไม่ใช่อาร์เรย์ แต่สังเกตได้ หากต้องการดูภาพเหล่านี้ เราจําเป็นต้องอัปเดตเทมเพลตของ AppComponent
เพียงแทนที่การเข้าถึงของพร็อพเพอร์ตี้ todo
, inProgress
และ done
ด้วย todo | async
, inProgress | async
และ done | async
ตามลําดับ
ไปป์แบบไม่พร้อมกันจะสมัครใช้บริการสิ่งที่สังเกตได้ที่เชื่อมโยงกับคอลเล็กชันโดยอัตโนมัติ เมื่อค่าที่สังเกตได้ปล่อยค่าใหม่ Angular จะเรียกใช้การตรวจหาการเปลี่ยนแปลงและประมวลผลอาร์เรย์ที่ปล่อยออกมาโดยอัตโนมัติ
เช่น เรามาดูการเปลี่ยนแปลงที่ต้องทําใน Swirl ของ 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>
เมื่อเราส่งข้อมูลไปยังคําสั่ง cdkDropList
เราจะใช้ไปป์ไลน์แบบไม่พร้อมกัน เหมือนกับในคําสั่ง *ngIf
แต่โปรดทราบว่าเราใช้ห่วงโซ่ที่ไม่บังคับ (หรือที่เรียกว่าโอเปอเรเตอร์การนําทางที่ปลอดภัยใน Angular) เมื่อเข้าถึงพร็อพเพอร์ตี้ length
เพื่อให้มั่นใจว่าเราจะไม่ได้รับข้อผิดพลาดเกี่ยวกับรันไทม์หาก todo | async
ไม่ใช่ null
หรือ undefined
เมื่อสร้างงานใหม่ในอินเทอร์เฟซผู้ใช้และเปิด Firestore คุณควรจะเห็นประมาณนี้
12. การปรับปรุงข้อมูลอัปเดตที่ดีที่สุด
ในแอปพลิเคชันใบสมัคร เรากําลังอัปเดตอย่างมีประสิทธิภาพ เรามีแหล่งความจริงใน Firestore แต่ในขณะเดียวกันก็มีสําเนางานในท้องถิ่น ซึ่งเมื่อมีการสังเกตการณ์ที่เกี่ยวกับคอลเล็กชันนี้ เราก็จะได้รับงานต่างๆ เมื่อการดําเนินการของผู้ใช้มีการเปลี่ยนแปลงสถานะ เราจะอัปเดตค่าในเครื่องก่อน แล้วจึงเผยแพร่การเปลี่ยนแปลงไปยัง Firestore
เมื่อเราย้ายงานจาก Swirl ไปยังอีก Chat หนึ่ง เราจะเรียกใช้ transferArrayItem,
ซึ่งทํางานในอินสแตนซ์อาร์เรย์ของอาร์เรย์ที่แสดงงานใน Swirl แต่ละรายการ Firebase SDK จะถือว่าอาร์เรย์เหล่านี้เป็นแบบแก้ไขไม่ได้ ซึ่งหมายความว่าในครั้งถัดไปที่ Angular เรียกใช้การตรวจหาการเปลี่ยนแปลง เราจะรับอินสแตนซ์ใหม่ของอาร์เรย์เหล่านั้น ซึ่งจะแสดงสถานะก่อนหน้าก่อนที่เราจะโอนงานดังกล่าว
ในขณะเดียวกัน เราทริกเกอร์การอัปเดต Firestore และ Firebase SDK จะเรียกให้การอัปเดตมีค่าที่ถูกต้อง ดังนั้น อินเทอร์เฟซผู้ใช้ดังกล่าวจะกลับสู่สถานะที่ถูกต้องภายใน 2-3 มิลลิวินาที ซึ่งทําให้งานที่คุณเพิ่งโอนไปข้ามรายการแรกไปยังรายการถัดไป คุณจะเห็นข้อมูลนี้อย่างถูกต้องใน GIF ด้านล่าง
วิธีที่เหมาะสมในการแก้ไขปัญหานี้จะแตกต่างกันไปในแต่ละแอปพลิเคชัน แต่ในทุกกรณี เราจําเป็นต้องรักษาสถานะให้คงที่จนกว่าจะมีการอัปเดตข้อมูล
เราใช้ประโยชน์จาก BehaviorSubject
ได้ ซึ่งสังเกตของผู้สังเกตการณ์เดิมที่ได้รับจาก valueChanges
BehaviorSubject
จะมีอาร์เรย์ที่เปลี่ยนแปลงได้ซึ่งคงการอัปเดตจาก transferArrayItem
ไว้ภายในขั้นสูง
เราเพียงต้องอัปเดต AppComponent
เพื่อดําเนินการแก้ไข ดังนี้
src/app/app.component.ts
...
import { AngularFirestore, AngularFirestoreCollection } from '@angular/fire/firestore';
import { BehaviorSubject } from 'rxjs';
const getObservable = (collection: AngularFirestoreCollection<Task>) => {
const subject = new BehaviorSubject<Task[]>([]);
collection.valueChanges({ idField: 'id' }).subscribe((val: Task[]) => {
subject.next(val);
});
return subject;
};
@Component(...)
export class AppComponent {
todo = getObservable(this.store.collection('todo')) as Observable<Task[]>;
inProgress = getObservable(this.store.collection('inProgress')) as Observable<Task[]>;
done = getObservable(this.store.collection('done')) as Observable<Task[]>;
...
}
สิ่งที่เราทําในข้อมูลโค้ดข้างต้นคือการสร้าง BehaviorSubject
ซึ่งจะแสดงค่าทุกครั้งที่สังเกตได้ที่เชื่อมโยงกับคอลเล็กชันมีการเปลี่ยนแปลง
ทุกอย่างทํางานได้ตามปกติ เนื่องจาก BehaviorSubject
นําอาร์เรย์มาใช้ซ้ําในการเรียกใช้การตรวจหาการเปลี่ยนแปลงและจะอัปเดตเมื่อได้รับค่าใหม่จาก Firestore เท่านั้น
13. การทำให้แอปพลิเคชันใช้งานได้
สิ่งที่ต้องทําเพื่อให้แอปใช้งานได้มีดังนี้
ng deploy
คําสั่งนี้จะ
- สร้างแอปด้วยการกําหนดค่าเวอร์ชันที่ใช้งานจริงโดยใช้การเพิ่มประสิทธิภาพเวลาคอมไพล์
- ทําให้แอปของคุณใช้งานได้บนโฮสติ้งของ Firebase
- พิมพ์ URL เพื่อให้ดูตัวอย่างผลลัพธ์ได้
14. ยินดีด้วย
ยินดีด้วย คุณสร้างกระดาน Kanan กับ Angular และ Firebase เรียบร้อยแล้ว
คุณสร้างอินเทอร์เฟซผู้ใช้ที่มี 3 คอลัมน์ ซึ่งแสดงสถานะของงานต่างๆ เมื่อใช้ Angular CDK คุณจะลากและวางงานในคอลัมน์ต่างๆ ได้ จากนั้นจึงใช้แบบฟอร์ม Angular เพื่อสร้างงานใหม่และแก้ไขงานที่มีอยู่ ถัดไป คุณได้ดูวิธีใช้ @angular/fire
และย้ายสถานะแอปพลิเคชันทั้งหมดไปยัง Firestore แล้ว ท้ายที่สุดแล้ว คุณได้ทําให้แอปพลิเคชันใช้งานได้ในโฮสติ้งของ Firebase
ขั้นตอนถัดไปที่ควรทํา
โปรดทราบว่าเรานําแอปพลิเคชันไปใช้งานโดยใช้การกําหนดค่าทดสอบ ก่อนที่จะทําให้แอปใช้งานได้ในเวอร์ชันที่ใช้งานจริง โปรดตรวจสอบว่าคุณได้ตั้งค่าสิทธิ์ที่ถูกต้องแล้ว โปรดดูวิธีการที่นี่
ปัจจุบันเราไม่เก็บลําดับของงานหนึ่งๆ ไว้ในช่องทําเครื่องหมายหนึ่งๆ หากต้องการนําไปใช้ ให้ใช้ช่องคําสั่งซื้อในเอกสารงานและจัดเรียงตามช่องนั้น
นอกจากนี้ เรายังสร้างบอร์ด Kanban ให้ผู้ใช้รายเดียว ซึ่งหมายความว่าเรามีบอร์ด Kanban อยู่ 1 ตัวสําหรับทุกคนที่เปิดแอป ทั้งนี้คุณจะต้องเปลี่ยนโครงสร้างฐานข้อมูล หากต้องการใช้บอร์ดแยกสําหรับผู้ใช้แต่ละคนในแอป ดูแนวทางปฏิบัติแนะนําของ Firestore' ได้ที่นี่