Mem-build aplikasi web dengan Angular dan Firebase

1. Pengantar

Terakhir Diperbarui: 11-09-2020

Yang akan Anda build

Dalam codelab ini, kita akan mem-build kanban board web dengan Angular dan Firebase. Aplikasi akhir kita akan memiliki tiga kategori tugas: backlog, dalam proses, dan selesai. Kita akan dapat membuat, menghapus tugas, dan mentransfernya dari satu kategori ke kategori lainnya dengan menggunakan tarik lalu lepas.

Kita akan mengembangkan antarmuka pengguna menggunakan Angular dan menggunakan Firestore sebagai penyimpanan persisten. Di akhir codelab, kita akan men-deploy aplikasi ke Firebase Hosting menggunakan Angular CLI.

b23bd3732d0206b.png

Yang akan Anda pelajari

  • Cara menggunakan materi Angular dan CDK.
  • Cara menambahkan integrasi Firebase ke aplikasi Angular Anda.
  • Cara mempertahankan data persisten Anda di Firestore.
  • Cara men-deploy aplikasi ke Firebase Hosting menggunakan Angular CLI dengan satu perintah.

Yang akan Anda perlukan

Codelab ini mengasumsikan bahwa Anda memiliki akun Google dan pemahaman dasar tentang Angular dan Angular CLI.

Mari kita mulai!

2. Membuat project baru

Pertama, mari kita buat ruang kerja Angular baru:

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

Langkah ini mungkin perlu waktu beberapa menit. Angular CLI membuat struktur project Anda dan menginstal semua dependensi. Saat proses penginstalan selesai, buka direktori kanban-fire dan mulai server pengembangan Angular CLI:

ng serve

Buka http://localhost:4200 dan seharusnya Anda melihat output seperti ini:

5ede7bc5b1109bf3.png

Di editor Anda, buka src/app/app.component.html dan hapus seluruh kontennya. Saat kembali ke http://localhost:4200, seharusnya Anda melihat halaman kosong.

3. Menambahkan Materi dan CDK

Angular dilengkapi dengan implementasi komponen antarmuka pengguna yang sesuai dengan desain material sebagai bagian dari paket @angular/material. Salah satu dependensi @angular/material adalah Component Development Kit, atau CDK. CDK menyediakan primitif, seperti utilitas a11y, tarik lalu lepas, dan overlay. Kita mendistribusikan CDK dalam paket @angular/cdk.

Untuk menambahkan materi ke aplikasi, jalankan:

ng add @angular/material

Perintah ini meminta Anda memilih tema, jika Anda ingin menggunakan gaya tipografi materi global, dan jika ingin menyiapkan animasi browser untuk Material Angular. Pilih "Indigo/Pink" untuk mendapatkan hasil yang sama seperti dalam codelab ini, dan jawab "Ya" untuk dua pertanyaan terakhir.

Perintah ng add menginstal @angular/material, dependensinya, serta mengimpor BrowserAnimationsModule di AppModule. Pada langkah berikutnya, kita dapat mulai menggunakan komponen yang ditawarkan modul ini.

Pertama, mari kita tambahkan toolbar dan ikon ke AppComponent. Buka app.component.html dan tambahkan markup berikut:

src/app/app.component.html

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

Di sini, kita menambahkan toolbar menggunakan warna utama tema desain material dan di dalamnya, kita menggunakan ikon local_fire_depeartment di samping label "Kanban Fire". Jika melihat konsol sekarang, Anda akan melihat bahwa Angular menampilkan beberapa error. Untuk memperbaikinya, pastikan Anda menambahkan impor berikut ke 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 { }

Karena menggunakan toolbar dan ikon material Angular, kita harus mengimpor modul yang sesuai di AppModule.

Di layar, seharusnya sekarang Anda melihat hal berikut:

a39cf8f8428a03bc.png

Hasil yang lumayan hanya dengan 4 baris HTML dan dua impor.

4. Memvisualisasikan tugas

Sebagai langkah berikutnya, mari kita buat komponen yang dapat kita gunakan untuk memvisualisasikan tugas di kanban board.

Buka direktori src/app dan jalankan perintah CLI berikut:

ng generate component task

Perintah ini menghasilkan TaskComponent dan menambahkan deklarasinya ke AppModule. Di dalam direktori task, buat file dengan nama task.ts. Kita akan menggunakan file ini untuk menentukan antarmuka tugas di kanban board. Setiap tugas akan memiliki kolom id, title, dan description opsional, semua string jenis:

src/app/task/task.ts

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

Sekarang mari kita perbarui task.component.ts. Kita ingin TaskComponent menerima objek input jenis Task, dan kita ingin elemen tersebut dapat menampilkan output "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>();
}

Edit template TaskComponent. Buka task.component.html dan ganti kontennya dengan HTML berikut:

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>

Perhatikan bahwa kita sekarang mendapatkan error di konsol:

'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

Dalam template di atas, kita menggunakan komponen mat-card dari @angular/material, tetapi kita belum mengimpor modul yang sesuai di aplikasi. Untuk memperbaiki error dari atas, kita perlu mengimpor MatCardModule di AppModule:

src/app/app.module.ts

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

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

Selanjutnya, kita akan membuat beberapa tugas di AppComponent dan memvisualisasikannya menggunakan TaskComponent.

Di AppComponent, tentukan array bernama todo dan di dalamnya tambahkan dua tugas:

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

Sekarang, di bagian bawah app.component.html tambahkan perintah *ngFor berikut:

src/app/app.component.html

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

Saat membuka browser, seharusnya Anda melihat tampilan berikut:

d96fccd13c63ceb1.png

5. Mengimplementasikan tarik lalu lepas untuk tugas

Kita akan memasuki bagian yang menyenangkan sekarang! Mari kita buat tiga swimlane untuk tiga tugas status yang berbeda, dan dengan menggunakan Angular CDK, implementasikan fungsi tarik lalu lepas.

Di app.component.html, hapus komponen app-task dengan perintah *ngFor di atasnya dan ganti dengan:

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>

Ada banyak hal yang terjadi. Mari kita lihat masing-masing bagian dari cuplikan ini langkah demi langkah. Ini adalah struktur tingkat teratas template:

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>

Di sini kita membuat div yang menggabungkan ketiga swimlane, dengan nama class "container-wrapper". Setiap swimlane memiliki nama class "container" dan judul di dalam tag h2.

Sekarang mari kita lihat struktur swimlane pertama:

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

Pertama, kita menentukan swimlane sebagai mat-card, yang menggunakan perintah cdkDropList. Kita menggunakan mat-card karena gaya yang disediakan oleh komponen ini. cdkDropList nantinya akan memungkinkan kita meletakkan tugas di dalam elemen. Kita juga menyetel dua input berikut:

  • cdkDropListData - input dari daftar lepas yang memungkinkan kita menentukan array data
  • cdkDropListConnectedTo - referensi ke cdkDropList lain yang terhubung dengan cdkDropList saat ini. Dengan menetapkan input ini, kita menentukan daftar lain tempat kita dapat menempatkan item

Selain itu, kita ingin menangani peristiwa lepas menggunakan output cdkDropListDropped. Setelah cdkDropList menampilkan output ini, kita akan memanggil metode drop yang dideklarasikan di dalam AppComponent dan meneruskan peristiwa saat ini sebagai argumen.

Perhatikan bahwa kita juga menentukan id yang akan digunakan sebagai ID untuk penampung ini, dan nama class sehingga kita dapat menata gayanya. Sekarang, mari kita lihat turunan konten mat-card. Dua elemen yang kita miliki adalah:

  • Paragraf, yang kita gunakan untuk menampilkan teks "Daftar kosong" jika tidak ada item di daftar todo
  • Komponen app-task. Perhatikan bahwa di sini kita menangani output edit yang awalnya kita deklarasikan dengan memanggil metode editTask dengan nama daftar dan objek $event. Ini akan membantu kita mengganti tugas yang diedit dari daftar yang benar. Selanjutnya, kita melakukan iterasi pada daftar todo seperti yang kita lakukan di atas dan meneruskan input task. Namun, kali ini, kita juga menambahkan perintah cdkDrag. Hal ini membuat tugas individu dapat ditarik.

Agar semua ini berfungsi, kita perlu memperbarui app.module.ts dan menyertakan impor ke DragDropModule:

src/app/app.module.ts

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

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

Kita juga perlu mendeklarasikan array inProgress dan done, bersama dengan metode editTask dan 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
    );
  }
}

Perhatikan bahwa dalam metode drop, kita terlebih dahulu memeriksa bahwa kita menempatkan tugas di daftar yang sama dengan asalnya. Jika demikian, maka kita segera kembali. Jika tidak, kita akan mentransfer tugas saat ini ke swimlane tujuan.

Hasilnya seharusnya:

460f86bcd10454cf.png

Pada tahap ini, seharusnya Anda dapat mentransfer item antara dua daftar.

6. Membuat tugas baru

Sekarang, mari kita implementasikan fungsi untuk membuat tugas baru. Untuk tujuan ini, mari kita perbarui template 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>

Kita membuat elemen div tingkat atas di sekitar container-wrapper dan menambahkan tombol dengan ikon material "add" di samping label "Tambahkan Tugas". Kita memerlukan wrapper tambahan untuk menempatkan tombol di atas daftar swimlane, yang nantinya akan ditempatkan secara bersebelahan menggunakan flexbox. Karena tombol ini menggunakan komponen tombol materi, kita perlu mengimpor modul yang sesuai di AppModule:

src/app/app.module.ts

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

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

Sekarang, mari kita implementasikan fungsi untuk menambahkan tugas di AppComponent. Kita akan menggunakan dialog materi. Dalam dialog, kita akan memiliki formulir dengan dua kolom: judul dan deskripsi. Saat pengguna mengklik tombol "Tambahkan Tugas", kita akan membuka dialog, dan saat pengguna mengirimkan formulir, kita akan menambahkan tugas yang baru dibuat ke daftar todo.

Mari kita lihat implementasi tingkat tinggi fungsi ini di 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);
      });
  }
}

Kita mendeklarasikan konstruktor tempat kita memasukkan class MatDialog. Di dalam newTask, kita:

  • Membuka dialog baru menggunakan TaskDialogComponent yang akan kita tentukan sebentar lagi.
  • Menetapkan bahwa kita ingin dialog memiliki lebar 270px.
  • Meneruskan tugas kosong ke dialog sebagai data. Di TaskDialogComponent, kita bisa mendapatkan referensi ke objek data ini.
  • Kita berlangganan peristiwa tutup dan menambahkan tugas dari objek result ke array todo.

Untuk memastikannya berfungsi, kita harus mengimpor MatDialogModule terlebih dahulu di AppModule:

src/app/app.module.ts

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

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

Sekarang, mari kita buat TaskDialogComponent. Buka direktori src/app dan jalankan:

ng generate component task-dialog

Untuk menerapkan fungsinya, buka terlebih dahulu: src/app/task-dialog/task-dialog.component.html dan ganti kontennya dengan:

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>

Pada template di atas, kita membuat formulir dengan dua kolom untuk title dan description. Kita menggunakan perintah cdkFocusInput untuk secara otomatis memfokuskan input title saat pengguna membuka dialog.

Perhatikan, bagaimana di dalam template, kita mereferensikan properti data komponen. Ini akan menjadi data yang sama yang kita teruskan ke metode open dari dialog di AppComponent. Untuk memperbarui judul dan deskripsi tugas saat pengguna mengubah konten kolom yang sesuai, kita menggunakan data binding dua arah dengan ngModel.

Saat pengguna mengklik tombol OK, kita akan secara otomatis menampilkan hasil { task: data.task }, yang merupakan tugas yang telah kita ubah menggunakan kolom formulir di template di atas.

Sekarang, mari kita implementasikan pengontrol komponen:

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

Dalam TaskDialogComponent, kita memasukkan referensi ke dialog sehingga kita dapat menutupnya, dan juga memasukkan nilai penyedia yang terkait dengan token MAT_DIALOG_DATA. Ini adalah objek data yang kita teruskan ke metode terbuka di AppComponent di atas. Kita juga mendeklarasikan backupTask properti pribadi, yang merupakan salinan tugas yang kita teruskan bersama dengan objek data.

Saat pengguna menekan tombol batal, kita akan memulihkan properti yang mungkin berubah dari this.data.task dan menutup dialog, sehingga meneruskan this.data sebagai hasilnya.

Ada dua jenis yang telah kita referensikan tetapi belum dideklarasikan - TaskDialogData dan TaskDialogResult. Di dalam src/app/task-dialog/task-dialog.component.ts, tambahkan deklarasi berikut ke bagian bawah file:

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

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

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

Hal terakhir yang perlu kita lakukan sebelum menyiapkan fungsionalitas adalah dengan mengimpor beberapa modul dalam 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 { }

Saat mengklik tombol "Tambahkan Tugas" sekarang, seharusnya Anda melihat antarmuka pengguna berikut:

33bcb987fade2a87.png

7. Meningkatkan gaya aplikasi

Untuk membuat aplikasi tampak lebih menarik secara visual, kita akan meningkatkan tata letaknya dengan sedikit memodifikasi gayanya. Kita ingin menempatkan swimlane di samping satu sama lain. Kita juga ingin beberapa penyesuaian kecil pada tombol "Tambahkan Tugas" dan label daftar kosong.

Buka src/app/app.component.css dan tambahkan gaya berikut ke bagian bawah:

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

Dalam cuplikan di atas, kita menyesuaikan tata letak toolbar dan labelnya. Kita juga memastikan bahwa konten disejajarkan secara horizontal dengan menyetel lebarnya ke 1400px dan marginnya ke auto. Selanjutnya, dengan menggunakan flexbox, kita menempatkan swimlane di samping satu sama lain, dan terakhir membuat beberapa penyesuaian dalam cara kita memvisualisasikan tugas dan daftar kosong.

Setelah aplikasi dimuat ulang, seharusnya Anda melihat antarmuka pengguna berikut:

69225f0b1aa5cb50.png

Meskipun kita secara signifikan meningkatkan gaya aplikasi, kita masih mengalami masalah yang mengganggu saat memindahkan tugas:

f9aae712027624af.png

Saat kita mulai menarik tugas "Beli susu", kita melihat dua kartu untuk tugas yang sama - yang kita tarik dan yang ada di swimlane. Angular CDK memberi kita nama class CSS yang dapat kita gunakan untuk memperbaiki masalah ini.

Tambahkan penggantian gaya berikut ke bagian bawah src/app/app.component.css:

src/app/app.component.css

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

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

Saat kita menarik elemen, tarik lalu lepas Angular CDK akan meng-clone dan menyisipkannya ke posisi tempat kita akan menempatkan elemen asli. Untuk memastikan elemen ini tidak terlihat, kita menetapkan properti opasitas di class cdk-drag-placeholder, yang akan ditambahkan CDK ke placeholder.

Selain itu, ketika kita melepaskan elemen, CDK akan menambahkan class cdk-drag-animating. Untuk menampilkan animasi yang mulus, bukan langsung mengambil elemen, kita tentukan transisi dengan durasi 250ms.

Kita juga ingin membuat beberapa penyesuaian kecil pada gaya tugas kita. Dalam task.component.css, mari kita tetapkan tampilan elemen host ke block dan tetapkan beberapa margin:

src/app/task/task.component.css

:host {
  display: block;
}

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

8 Mengedit dan menghapus tugas yang ada

Untuk mengedit dan menghapus tugas yang ada, kita akan menggunakan kembali sebagian besar fungsi yang sudah kita terapkan. Saat pengguna mengklik dua kali tugas, kita akan membuka TaskDialogComponent dan mengisi dua kolom dalam formulir dengan title dan description tugas.

Kita juga akan menambahkan tombol hapus ke TaskDialogComponent. Saat pengguna mengkliknya, kita akan meneruskan petunjuk penghapusan, yang akan berakhir di AppComponent.

Satu-satunya perubahan yang perlu kita buat di TaskDialogComponent adalah pada templatenya:

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>

Tombol ini menampilkan ikon material hapus. Saat pengguna mengkliknya, kita akan menutup dialog dan meneruskan objek literal { task: data.task, delete: true } sebagai hasilnya. Perhatikan juga bahwa kita membuat tombol berbentuk lingkaran menggunakan mat-fab, menetapkan warnanya menjadi utama, dan menampilkannya hanya jika data dialog mengaktifkan penghapusan.

Implementasi lainnya untuk fungsi edit dan hapus ada di AppComponent. Ganti metode editTask dengan metode berikut:

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

Mari kita lihat argumen metode editTask:

  • Daftar jenis 'done' | 'todo' | 'inProgress', yang merupakan jenis gabungan literal string dengan nilai yang sesuai dengan properti yang terkait dengan swimlane individu.
  • Tugas saat ini yang ingin kita edit.

Dalam isi metode, pertama-tama kita akan membuka instance TaskDialogComponent. Sebagai data, kita meneruskan objek literal, yang menentukan tugas yang ingin kita edit, dan juga mengaktifkan tombol edit di formulir dengan menyetel properti enableDelete ke true.

Saat kita mendapatkan hasil dari dialog, kita menangani dua skenario:

  • Jika tanda delete ditetapkan ke true (yaitu, saat pengguna telah menekan tombol hapus), kita akan menghapus tugas dari daftar yang sesuai.
  • Atau, kita cukup mengganti tugas pada indeks tertentu dengan tugas kita dapatkan dari hasil dialog.

9. Membuat project Firebase baru

Sekarang, mari kita buat project Firebase baru.

10. Menambahkan Firebase ke project

Di bagian ini, kita akan mengintegrasikan project dengan Firebase. Tim Firebase menawarkan paket @angular/fire, yang menyediakan integrasi antara dua teknologi. Untuk menambahkan dukungan Firebase ke aplikasi Anda, buka direktori utama ruang kerja Anda dan jalankan:

ng add @angular/fire

Perintah ini menginstal paket @angular/fire dan mengajukan beberapa pertanyaan kepada Anda. Di terminal, seharusnya Anda melihat sesuatu seperti:

9ba88c0d52d18d0.png

Sementara itu, penginstalan membuka jendela browser sehingga Anda dapat mengautentikasi dengan akun Firebase. Terakhir, Anda diminta untuk memilih project Firebase dan membuat beberapa file di disk.

Selanjutnya, kita perlu membuat database Firestore. Di bagian "Cloud Firestore", klik "Buat Database".

1e4a08b5a2462956.png

Setelah itu, buat database dalam mode pengujian:

ac1181b2c32049f9.png

Terakhir, pilih wilayah:

34bb94cc542a0597.png

Sekarang, Anda hanya perlu menambahkan konfigurasi Firebase ke lingkungan. Anda dapat menemukan konfigurasi project di Firebase Console.

  • Klik ikon Roda Gigi di samping Ringkasan Project.
  • Pilih Setelan Project.

c8253a20031de8a9.png

Di bagian "Aplikasi Anda", pilih "Aplikasi web":

428a1abcd0f90b23.png

Selanjutnya, daftarkan aplikasi dan pastikan Anda mengaktifkan "Firebase Hosting":

586e44cb27dd8f39.png

Setelah mengklik "Daftarkan aplikasi", Anda dapat menyalin konfigurasi ke src/environments/environment.ts:

e30f142d79cecf8f.png

Di bagian akhir, file konfigurasi seharusnya terlihat seperti ini:

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. Memindahkan data ke Firestore

Setelah menyiapkan Firebase SDK, mari kita gunakan @angular/fire untuk memindahkan data ke Firestore. Pertama, mari kita impor modul yang kita perlukan di 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 {}

Karena kita akan menggunakan Firestore, kita perlu memasukkan AngularFirestore ke dalam konstruktor AppComponent:

src/app/app.component.ts

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

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

Selanjutnya, kita memperbarui cara kita melakukan inisialisasi array swimlane:

src/app/app.component.ts

...

@Component({...})
export class AppComponent {
  todo = this.store.collection('todo').valueChanges({ idField: 'id' }) as Observable<Task[]>;
  inProgress = this.store.collection('inProgress').valueChanges({ idField: 'id' }) as Observable<Task[]>;
  done = this.store.collection('done').valueChanges({ idField: 'id' }) as Observable<Task[]>;
  ...
}

Di sini, kita menggunakan AngularFirestore untuk mendapatkan konten koleksi secara langsung dari database. Perhatikan bahwa valueChanges menampilkan observable, bukan array, dan juga menentukan kolom id untuk dokumen dalam koleksi ini harus bernama id agar cocok dengan nama yang kita gunakan dalam antarmuka Task. Observable yang ditampilkan oleh valueChanges memunculkan kumpulan tugas setiap kali berubah.

Karena kita menggunakan observable, bukan array, kita perlu memperbarui cara kita menambah, menghapus, dan mengedit tugas, dan fungsi untuk memindahkan tugas antara swimlane. Kita akan menggunakan Firebase SDK untuk memperbarui data di database, bukan mengubah array dalam memori.

Pertama-tama, mari kita lihat tampilan penyusunan ulang. Ganti metode drop di src/app/app.component.ts dengan:

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

Dalam cuplikan di atas, kode baru ditandai. Untuk memindahkan tugas dari swimlane saat ini ke target, kita akan menghapus tugas dari koleksi pertama dan menambahkannya ke koleksi kedua. Karena kita melakukan dua operasi yang kita inginkan seperti satu operasi (yaitu, membuat operasi atomik), kita menjalankannya di transaksi Firestore.

Selanjutnya, mari kita perbarui metode editTask untuk menggunakan Firestore. Di dalam pengendali dialog tutup, kita harus mengubah baris kode berikut:

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

Kita mengakses dokumen target sesuai dengan tugas yang kita manipulasi menggunakan Firestore SDK dan menghapus atau memperbaruinya.

Terakhir, kita perlu memperbarui metode untuk membuat tugas baru. Ganti this.todo.push('task') dengan: this.store.collection('todo').add(result.task).

Perhatikan bahwa sekarang koleksi kita bukanlah array, tetapi observable. Untuk dapat memvisualisasikannya, kita perlu memperbarui template AppComponent. Cukup ganti setiap akses properti todo, inProgress, dan done dengan todo | async, inProgress | async, dan done | async.

Pipa asinkron secara otomatis berlangganan ke observable yang terkait dengan koleksi tersebut. Saat observable menampilkan nilai baru, Angular secara otomatis menjalankan deteksi perubahan dan memproses array yang dimunculkan.

Misalnya, mari kita lihat perubahan yang perlu dilakukan di swimlane 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>

Saat meneruskan data ke perintah cdkDropList, kita akan menerapkan pipa asinkron. Ini sama di dalam perintah *ngIf, tetapi perhatikan bahwa di sana kita juga menggunakan rantai opsional (juga dikenal sebagai operator navigasi aman di Angular), saat mengakses properti length untuk memastikan kita tidak mendapatkan error runtime jika todo | async bukan null atau undefined.

Sekarang, saat membuat tugas baru di antarmuka pengguna dan membuka Firestore, seharusnya Anda melihat tampilan seperti ini:

dd7ee20c0a10ebe2.png

12. Meningkatkan update optimis

Di aplikasi, kita saat ini melakukan update optimis. Kita memiliki sumber tepercaya di Firestore, tetapi pada saat yang sama kita memiliki salinan tugas lokal; saat salah satu observable yang terkait dengan koleksi tersebut muncul, kita mendapatkan array tugas. Saat tindakan pengguna mengubah status, pertama-tama kita akan memperbarui nilai lokal, lalu menerapkan perubahan ke Firestore.

Saat memindahkan tugas dari satu swimlane ke swimlane lainnya, kita memanggil transferArrayItem, yang beroperasi pada instance lokal array yang merepresentasikan tugas di setiap swimlane. Firebase SDK menganggap array ini sebagai tidak dapat diubah, yang berarti bahwa saat berikutnya Angular menjalankan deteksi perubahan, kita akan mendapatkan instance baru darinya, yang akan merender status sebelumnya sebelum kita mentransfer tugas.

Pada saat yang sama, kita memicu update Firestore dan Firebase SDK memicu update dengan nilai yang benar, sehingga dalam beberapa milidetik antarmuka pengguna akan kembali ke status yang benar. Ini membuat tugas yang baru saja kita transfer melompat dari daftar pertama ke daftar berikutnya. Anda dapat melihatnya dengan baik pada GIF di bawah ini:

70b946eebfa6f316.gif

Cara yang tepat untuk menyelesaikan masalah ini bervariasi dari satu aplikasi ke aplikasi lain, tetapi dalam semua kasus, kita perlu memastikan bahwa kita mempertahankan status yang konsisten sampai data kita diperbarui.

Kita dapat memanfaatkan BehaviorSubject, yang menggabungkan pengamat asli yang kita terima dari valueChanges. Di balik layar, BehaviorSubject menyimpan array yang dapat diubah yang mempertahankan update dari transferArrayItem.

Untuk menerapkan perbaikan, yang perlu kita lakukan adalah mengupdate 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[]>;
...
}

Yang kita lakukan dalam cuplikan di atas adalah membuat BehaviorSubject, yang memunculkan nilai setiap kali observable yang terkait dengan koleksi berubah.

Semuanya berfungsi seperti yang diharapkan, karena BehaviorSubject menggunakan kembali array di seluruh panggilan deteksi perubahan dan hanya mengupdate saat kita mendapatkan nilai baru dari Firestore.

13. Men-deploy aplikasi

Yang perlu kita lakukan untuk men-deploy aplikasi adalah jalankan:

ng deploy

Perintah ini akan:

  1. Mem-build aplikasi Anda dengan konfigurasi produksinya, yang menerapkan pengoptimalan waktu kompilasi.
  2. Men-deploy aplikasi Anda ke Firebase Hosting.
  3. Menghasilkan output URL agar Anda dapat melihat pratinjau hasilnya.

14. Selamat

Selamat, Anda telah berhasil membuat kanban board dengan Angular dan Firebase.

Anda telah membuat antarmuka pengguna dengan tiga kolom yang mewakili status tugas yang berbeda. Dengan menggunakan Angular CDK, Anda telah menerapkan fitur tarik lalu lepas tugas di seluruh kolom. Kemudian, dengan menggunakan materi Angular, Anda telah membuat formulir untuk membuat tugas baru dan mengedit tugas yang sudah ada. Selanjutnya, Anda telah mempelajari cara menggunakan @angular/fire dan memindahkan semua status aplikasi ke Firestore. Terakhir, Anda men-deploy aplikasi ke Firebase Hosting.

Apa selanjutnya?

Ingat bahwa kita men-deploy aplikasi menggunakan konfigurasi pengujian. Sebelum men-deploy aplikasi ke produksi, pastikan Anda menyiapkan izin yang benar. Anda dapat mempelajari cara melakukannya di sini.

Saat ini, kita tidak mempertahankan urutan setiap tugas di swimlane tertentu. Untuk menerapkannya, Anda dapat menggunakan kolom urutan di dokumen tugas dan mengurutkannya.

Selain itu, kita mem-build kanban board hanya untuk satu pengguna, yang berarti kita memiliki satu kanban board untuk siapa saja yang membuka aplikasi. Untuk menerapkan board terpisah untuk pengguna aplikasi yang berbeda, Anda harus mengubah struktur database. Pelajari tentang praktik terbaik Firestore di sini.