Angular ve Firebase ile web uygulaması oluşturma

1. Giriş

Son Güncelleme: 11.09.2020

Geliştireceğiniz uygulama

Bu codelab'de, Angular ve Firebase ile web kanban panosu oluşturacağız. Son uygulamamızda yığın, devam eden ve tamamlanan olmak üzere üç görev kategorisi mevcuttur. Sürükle ve bırak özelliğini kullanarak görevleri oluşturabilecek, silebilecek ve bir kategoriden diğerine aktarabileceğiz.

Kullanıcı arayüzünü Angular kullanarak geliştirecek ve Firestore'u kalıcı mağazamız olarak kullanacağız. Codelab'in sonunda, uygulamayı Angular KSA'yı kullanarak Firebase Hosting'e dağıtacağız.

b23bd3732d0206b.png

Neler öğreneceksiniz?

  • Angular materyali ve CDK'yi kullanma.
  • Angular uygulamanıza Firebase entegrasyonunu ekleme.
  • Kalıcı verilerinizi Firestore'da tutma.
  • Angular KSA'yı kullanarak tek bir komutla uygulamanızı Firebase Hosting'e dağıtma.

Gerekenler

Bu codelab'de, Google hesabınızın olduğunu ve Angular ile Angular KSA'yı temel olarak anladığınızı varsayıyoruz.

Haydi başlayalım!

2. Yeni proje oluşturma

Öncelikle yeni bir Angular çalışma alanı oluşturalım:

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

Bu adım birkaç dakika sürebilir. Angular KSA, proje yapınızı oluşturur ve tüm bağımlılıkları yükler. Yükleme işlemi tamamlandığında kanban-fire dizinine gidin ve Angular CLI''ın geliştirme sunucusunu başlatın:

ng serve

http://localhost:4200 sayfasını açtığınızda şuna benzer bir çıkış görürsünüz:

5ede7bc5b1109bf3.png

Düzenleyicinizde src/app/app.component.html uygulamasını açın ve içeriğinin tamamını silin. http://localhost:4200 adresine geri döndüğünüzde boş bir sayfa görmeniz gerekir.

3. CDK ve CDK Ekleme

Angular, @angular/material paketinin bir parçası olarak malzeme tasarımına uygun kullanıcı arayüzü bileşenlerinin uygulanmasıyla gelir. @angular/material bağımlılarından biri, Bileşen Geliştirme Kiti veya CDK'dir. CDK; a11y yardımcı programları, sürükle ve bırak ve yer paylaşımı gibi temel bilgiler sağlar. CDK'yi @angular/cdk paketinde dağıtırız.

Uygulama çalıştırmanıza materyal eklemek için:

ng add @angular/material

Genel malzeme yazı stili stillerini kullanmak ve tarayıcı animasyonlarını Angular Materyali için ayarlamak istiyorsanız bu komutu kullanarak tema belirlemeniz istenir. Bu codelab'deki sonuçla aynı sonucu elde etmek için "Indigo/Pink"i seçin ve son iki soruya "Evet" yanıtını verin.

ng add komutu @angular/material uygulamasını ve bağımlılarını yükler ve BrowserAnimationsModule öğesini AppModule içe aktarır. Bir sonraki adımda, bu modülün sunduğu bileşenleri kullanmaya başlayabiliriz.

Öncelikle AppComponent öğesine bir araç çubuğu ve simge ekleyelim. app.component.html öğesini açın ve aşağıdaki işaretlemeyi ekleyin:

src/app/app.component.html

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

Burada, malzeme tasarımı temamızın birincil rengini kullanarak araç çubuğu ekliyoruz. İçinde, "Kanban Fire" etiketinin yanındaki local_fire_depeartment simgesini kullanıyoruz. Konsolunuza şimdi bakarsanız Angular'ın birkaç hataya neden olduğunu görürsünüz. Bunları düzeltmek için AppModule ürününe aşağıdaki içe aktarmaları eklediğinizden emin olun:

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

Açılı malzeme araç çubuğu ve simgesi kullandığımız için ilgili modülleri AppModule içinde içe aktarmamız gerekiyor.

Ekranda artık şunları görmeniz gerekir:

a39cf8f8428a03bc.png

Yalnızca 4 satır HTML ve iki içe aktarma işlemi için sorun yoktur.

4. Görevleri görselleştirme

Sonraki adım olarak, kanban panosundaki görevleri görselleştirmek için kullanabileceğimiz bir bileşen oluşturalım.

src/app dizinine gidin ve aşağıdaki CLI komutunu çalıştırın:

ng generate component task

Bu komut, TaskComponent oluşturur ve beyanını AppModule hedefine ekler. task dizininin içinde task.ts adlı bir dosya oluşturun. Bu dosyayı kanban panosundaki görevlerin arayüzünü tanımlamak için kullanacağız. Her görevde isteğe bağlı olarak id, title ve description alanlarının tümü dize türünde olacaktır:

src/app/task/task.ts

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

Şimdi task.component.ts uygulamasını güncelleyelim. TaskComponent öğesinin Task türünde bir nesne olarak kabul etmesini ve "&edit; çıktıları yayabilmesini isteriz:

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 adlı kullanıcının şablonunu düzenleyin. task.component.html uygulamasını açıp içeriğini aşağıdaki HTML ile değiştirin:

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>

Konsolda artık hata alıyoruz:

'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

Yukarıdaki şablonda, @angular/material şablonundaki mat-card bileşenini kullanıyoruz, ancak ilgili modülü uygulamada içe aktarmadık. Yukarıdaki hatayı düzeltmek için AppModule içinde MatCardModule öğesini içe aktarmamız gerekiyor:

src/app/app.module.ts

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

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

Şimdi, AppComponent içinde birkaç görev oluşturalım ve TaskComponent kullanarak bunları görselleştirelim.

AppComponent öğesinde todo adlı bir dizi tanımlayın ve içine iki görev ekleyin:

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

Şimdi, app.component.html sayfasının alt kısmına aşağıdaki *ngFor yönergesini ekleyin:

src/app/app.component.html

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

Tarayıcıyı açtığınızda şunları görürsünüz:

d96fccd13c63ceb1.png

5. Görevler için sürükle ve bırak özelliğini uygulama

Şimdi de eğlenceli kısım için hazırız. Görevlerin içinde bulunabileceği üç farklı eyalet için üç ayrı köprü oluşturun ve Angular CDK'yi kullanarak sürükle ve bırak işlevini uygulayın.

app.component.html etiketinde app-task bileşenini (*ngFor) üstten aşağıdakiyle değiştirin:

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>

Burada çok şey var. Bu snippet'in her bir adımını adım adım inceleyelim. Bu, şablonun en üst düzey yapısıdır:

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>

Burada her üç köprüyü de sınıf adı "container-wrapper." ile sarmalayan bir div oluştururuz. Her plaj malzemesinin sınıf adı "container&quot" ve h2 etiketi içinde bir başlığı vardır.

Şimdi ilk yüzme şeridinin yapısına bakalım:

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

İlk olarak, köprüyü, cdkDropList yönergesini kullanan bir mat-card olarak tanımlıyoruz. Bu bileşenin sağladığı stiller nedeniyle bir mat-card kullanıyoruz. cdkDropList, daha sonra görevin içindeki görevleri bırakmamıza olanak tanır. Aşağıdaki iki girişi de belirledik:

  • cdkDropListData - veri dizisini belirtmemize olanak tanıyan açılır liste girişi
  • cdkDropListConnectedTo - geçerli cdkDropList öğesinin bağlı olduğu diğer cdkDropList'lere referans verir. Bu girişi ayarladığınızda, öğeleri dahil edebileceğimiz diğer listeleri belirtiriz

Ayrıca, cdkDropListDropped çıktısını kullanarak düşüş etkinliğini işlemek istiyoruz. cdkDropList bu çıkışı oluşturduğunda, AppComponent içinde beyan edilen drop yöntemini çağırıp mevcut etkinliği bağımsız değişken olarak iletiriz.

Ayrıca, bu kapsayıcının tanımlayıcısı olarak kullanılacak bir id ve stili değiştirebileceğimiz bir class adı da belirtiriz. Şimdi mat-card tarafından sağlanan içeriğe göz atalım. Elimizdeki iki öğe vardır:

  • todo listesinde hiç öğe olmadığında "Boş liste" metnini göstermek için kullandığımız bir paragraftır
  • app-task bileşeni. Burada, liste adını ve $event nesnesini içeren editTask yöntemini çağırarak başlangıçta belirttiğimiz edit çıkışını işlediğimize dikkat edin. Bu işlem, düzenlenmiş görevi doğru listeden değiştirmemize yardımcı olur. Ardından, yukarıda olduğu gibi todo listesini tekrarlayıp task girişini geçiririz. Ancak bu kez cdkDrag yönergesini de ekliyoruz. Tek tek görevleri sürüklenebilir hale getirir.

Tüm bu işlemleri gerçekleştirmek için app.module.ts öğesini güncellememiz ve DragDropModule öğesine bir içe aktarma işlemi eklememiz gerekir:

src/app/app.module.ts

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

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

Ayrıca inProgress ve done dizilerini, editTask ve drop yöntemleriyle birlikte tanımlamamız gerekir:

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 yönteminde, öncelikle görevin geldiği yerle aynı olup olmadığını kontrol ettiğimize dikkat edin. Bu durumda hemen geri döneriz. Aksi takdirde mevcut görev, hedef kordona aktarılır.

Sonuç şöyle olmalıdır:

460f86bcd10454cf.png

Bu noktada, iki liste arasında zaten öğe aktarabiliyor olmanız gerekir.

6. Yeni görev oluşturma

Şimdi yeni görevler oluşturmak için bir işlev uygulayalım. Bu amaçla, AppComponent şablonunu güncelleyeceğiz:

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>

container-wrapper çevresinde üst düzey bir div öğesi oluşturur ve bir etiketin "Görev Ekle" düğmesinin yanına "add" malzeme simgesini içeren bir düğme ekleriz. Düğmeyi yüzme şeridi listesinin üzerine yerleştirmek için ek sarmalayıcıya ihtiyacımız var. Bunu daha sonra flexbox'ı kullanarak yan yana yerleştireceğiz. Bu düğmede malzeme düğmesi bileşeni kullanıldığı için ilgili modülü AppModule ürününe aktarmamız gerekiyor:

src/app/app.module.ts

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

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

Şimdi AppComponent işlevine görev ekleme işlevini uygulayalım. Malzeme iletişim kutusunu kullanacağız. İletişim kutusunda iki alan içeren bir formumuz olacak: başlık ve açıklama. Kullanıcı "Görev Ekle" düğmesini tıkladığında iletişim kutusunu açıyoruz. Kullanıcı da formu gönderdiğinde yeni oluşturulan görevi todo listesine ekliyoruz.

AppComponent ürünündeki bu işlevin üst düzey uygulamasına göz atalım:

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 sınıfını yerleştirdiğimiz bir oluşturucuyu bildiririz. newTask içinde:

  • Birazdan tanımlayacağınız TaskDialogComponent öğesini kullanarak yeni bir iletişim kutusu açın.
  • İletişim kutusunun genişliğinin 270px. olmasını istediğimizi belirtin
  • İletişim kutusuna veri olarak boş bir görev iletin. TaskDialogComponent içinde bu veri nesnesine referans oluşturabileceğiz.
  • Kapatma etkinliğine abone olur ve result nesnesinden görevi todo dizisine ekleriz.

Bunun işe yaraması için önce AppModule içindeki MatDialogModule öğesini içe aktarmamız gerekir:

src/app/app.module.ts

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

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

Şimdi TaskDialogComponent oluşturalım. src/app dizinine gidin ve şunu çalıştırın:

ng generate component task-dialog

İşlevlerini uygulamak için önce src/app/task-dialog/task-dialog.component.html sayfasını açın ve içeriğini aşağıdakilerle değiştirin:

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>

Yukarıdaki şablonda, title ve description için iki alan içeren bir form oluştururuz. Kullanıcı iletişim kutusunu açtığında title girişine otomatik olarak odaklanmak için cdkFocusInput yönergesini kullanırız.

Şablonun içinde, bileşenin data özelliğine nasıl atıfta bulunduğumuza dikkat edin. Bu, AppComponent içinde dialog öğesinin open yöntemine ilettiğimiz data ile aynıdır. Kullanıcı ilgili alanların içeriğini değiştirdiğinde görevin başlığını ve açıklamasını güncellemek için ngModel ile iki yönlü veri bağlama özelliğini kullanırız.

Kullanıcı Tamam düğmesini tıkladığında otomatik olarak { task: data.task } sonucunu döndürür. Bu, yukarıdaki şablonda bulunan form alanlarını kullanarak yoksaydığımız görevdir.

Şimdi bileşenin denetleyicisini uygulayalım:

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 içinde, iletişim kutusuna yönelik bir referans ekliyoruz. Bu iletişim kutusunu kapatabiliyoruz ve ayrıca MAT_DIALOG_DATA jetonuyla ilişkilendirilmiş sağlayıcının değerini de ekliyoruz. Bu, yukarıdaki AppComponent içinde açık yönteme ilettiğimiz veri nesnesidir. Ayrıca, veri nesnesiyle birlikte ilettiğimiz görevin bir kopyası olan backupTask özel özelliğini de beyan ederiz.

Kullanıcı iptal düğmesine bastığında, this.data.task ürününün muhtemelen değiştirilmiş özelliklerini geri yükler ve iletişim kutusunu kapatarak this.data sonucunu alırız.

Belirttiğimiz ancak henüz bildirmediğimiz iki tür vardır: TaskDialogData ve TaskDialogResult. src/app/task-dialog/task-dialog.component.ts içinde, aşağıdaki bildirimleri dosyanın en altına ekleyin:

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

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

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

İşlevi hazır hale getirmeden önce yapmamız gereken son işlem, AppModule modülünde birkaç modül içe aktarmaktır.

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

Şimdi "Görev Ekle" düğmesini tıkladığınızda aşağıdaki kullanıcı arayüzünü görürsünüz:

33bcb987fade2a87.png

7. Uygulamanın stillerini iyileştirme

Uygulamayı daha görsel hale getirmek için stillerinde küçük değişiklikler yaparak düzenini iyileştireceğiz. Maymunları yan yana konumlandırmak istiyoruz. "Görev Ekle" düğmesi ve boş liste etiketinde bazı küçük düzenlemeler de yapmak istiyoruz.

src/app/app.component.css öğesini açın ve aşağıdaki stilleri alta ekleyin:

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

Yukarıdaki snippet'te, araç çubuğunun ve etiketinin düzenini ayarlarız. Ayrıca, genişliği 1400px ve marjı auto olarak ayarlayarak içeriğin yatay olarak hizalanmasını da sağlarız. Daha sonra, flexbox'ı kullanarak şemsiyeleri yan yana koyduk ve son olarak görevleri ve boş listeleri nasıl görselleştireceğimizle ilgili bazı düzenlemeler yaptık.

Uygulamanız yeniden yüklendiğinde aşağıdaki kullanıcı arayüzünü göreceksiniz:

69225f0b1aa5cb50.png

Uygulamalarımızın stillerini önemli ölçüde iyileştirmiş olsak da, görevleri taşıdığımızda hâlâ rahatsız edici bir sorun yaşıyoruz:

f9aae712027624af.png

"süt satın al" görevini sürüklemeye başladığımızda aynı görev için iki kart görürüz: birer kart sürüklediğimiz ve yüzen kartta. Angular CDK, bu sorunu düzeltmek için kullanabileceğimiz CSS sınıf adlarını sağlar.

src/app/app.component.css öğesinin alt kısmına aşağıdaki stil geçersiz kılmalarını ekleyin:

src/app/app.component.css

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

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

Bir öğeyi sürüklerken Angular CDK&#39'u sürükleyip klonlar ve orijinali bırakacağımız konuma yerleştirir. Bu öğenin görünür olmadığından emin olmak için cdk-drag-placeholder sınıfında opaklık özelliğini ayarlarız. CDK, yer tutucuya ekler.

Ayrıca bir öğeyi bıraktığımızda CDK, cdk-drag-animating sınıfını ekler. Öğeyi doğrudan tutturmak yerine yumuşak bir animasyon göstermek için 250ms süresine sahip bir geçiş tanımlarız.

Ayrıca, görevlerimizin stillerinde birkaç küçük ayarlama yapmak istiyoruz. task.component.css adlı cihazda düzenleyen ana makinesi ekranını block ve bazı kenar boşluklarını da belirleyelim:

src/app/task/task.component.css

:host {
  display: block;
}

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

8. Mevcut görevleri düzenleme ve silme

Mevcut görevleri düzenlemek ve kaldırmak için, uygulamaya almış olduğumuz işlevlerin çoğunu yeniden kullanacağız. Kullanıcı bir görevi çift tıkladığında TaskDialogComponent öğesini açar ve formdaki iki alanı görevin title ve description alanıyla doldurur.

Ayrıca TaskDialogComponent cihazına bir sil düğmesi ekleriz. Kullanıcı bu uzantıyı tıkladığında bir silme talimatı göndeririz. AppComponent talimatı

TaskDialogComponent ürününde yapmamız gereken tek değişiklik, şablonudur:

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>

Bu düğme, malzemeyi sil simgesini gösterir. Kullanıcı bu düğmeyi tıkladığında iletişim kutusunu kapatır ve sonuç olarak { task: data.task, delete: true } değişmez değerini iletiriz. Ayrıca, düğmeyi mat-fab kullanarak dairesel yaptığımızı, rengini birincil olarak ayarladığımızı ve iletişim kutusunda veri silme seçeneği etkin olduğunda gösterdiğimizi de unutmayın.

Düzenleme ve silme işlevlerinin geri kalanı AppComponent üzerinden sunulur. editTask yöntemini aşağıdakiyle değiştirin:

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 yönteminin bağımsız değişkenlerine bakalım:

  • Tekli kordonlarla ilişkili özelliklere karşılık gelen, dize değişmez değerili bir tür olan 'done' | 'todo' | 'inProgress', türünün listesi.
  • Düzenlemek istediğimiz mevcut görev.

Yöntemin gövdesinde ilk olarak TaskDialogComponent öğesinin bir örneğini açarız. data olarak, düzenlemek istediğimiz görevi belirten bir nesne değişmez değeri iletiyoruz. Ayrıca, enableDelete özelliğini true olarak ayarlayarak formdaki düzenle düğmesini etkinleştiriyoruz.

İletişim kutusundan sonucu aldığımızda iki senaryoyu ele alırız:

  • delete işareti true olarak ayarlandığında (kullanıcı sil düğmesine bastığında) görevi ilgili listeden kaldırırız.
  • Alternatif olarak, söz konusu dizindeki görevi iletişim kutusu sonucundan aldığımız görevle değiştiriyoruz.

9. Yeni Firebase projesi oluşturma

Şimdi yeni bir Firebase projesi oluşturalım.

10. Firebase, projeye ekleniyor

Bu bölümde projemizi Firebase'e entegre edeceğiz. Firebase ekibi, iki teknoloji arasında entegrasyon sağlayan @angular/fire paketini sunar. Uygulamanıza Firebase desteği eklemek için çalışma alanınızın kök dizinini açın ve şunu çalıştırın:

ng add @angular/fire

Bu komut, @angular/fire paketini yükler ve size birkaç soru sorar. Terminalinizde aşağıdaki gibi bir şey görürsünüz:

9ba88c0d52d18d0.png

Bu arada, yükleme sırasında Firebase hesabınızla kimlik doğrulaması yapabilmeniz için bir tarayıcı penceresi açılır. Son olarak, bir Firebase projesi seçmenizi ister ve diskinizde bazı dosyalar oluşturur.

Şimdi, bir Firestore veritabanı oluşturmamız gerekiyor. "Cloud Firestore"un altında "Veritabanı Oluştur"u tıklayın.

1e4a08b5a2462956.png

Ardından, test modunda bir veritabanı oluşturun:

ac1181b2c32049f9.png

Son olarak, bir bölge seçin:

34bb94cc542a0597.png

Şimdi tek yapmanız gereken Firebase yapılandırmasını ortamınıza eklemek. Proje yapılandırmanızı Firebase Console'da bulabilirsiniz.

  • Projeye Genel Bakış'ın yanındaki Dişli simgesini tıklayın.
  • Proje Ayarları'nı seçin.

c8253a20031de8a9.png

"Uygulamanız" bölümünün altında "Web uygulaması"nı seçin:

428a1abcd0f90b23.png

Ardından, uygulamanızı kaydettirin ve "Firebase Hosting"i etkinleştirdiğinizden emin olun:

586e44cb27dd8f39.png

"Uygulamayı kaydet"i tıkladıktan sonra yapılandırmanızı src/environments/environment.ts ürününe kopyalayabilirsiniz:

e30f142d79cecf8f.png

Sonunda, yapılandırma dosyanız şu şekilde görünmelidir:

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. Verileri Firestore'a taşıma

Firebase SDK'sını ayarladığımıza göre artık verilerimizi Firestore'a taşımak için @angular/fire özelliğini kullanabiliriz. Öncelikle AppModule ürününde ihtiyacımız olan modülleri içe aktaralım:

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'u kullanacağımız için AngularFirestore cihazını AppComponent nesnesine eklememiz gerekiyor:

src/app/app.component.ts

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

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

Ardından, yüzme şeridi dizilerini başlatma yöntemimizi güncelliyoruz:

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

Burada koleksiyonun içeriğini doğrudan veritabanından almak için AngularFirestore öğesini kullanırız. valueChanges öğesinin, dizi yerine gözlemlenebilir değer döndürdüğünü ve bu koleksiyondaki belgeler için kimlik alanının, Task arayüzünde kullandığımız adla eşleşecek şekilde id olarak adlandırılması gerektiğini unutmayın. valueChanges tarafından döndürülen gözlemlenebilir durum, her değiştiğinde bir görev koleksiyonu oluşturur.

Diziler yerine gözlemlenebilir özellikler üzerinde çalıştığımız için görevleri ekleme, kaldırma ve düzenleme yöntemlerimizi ve görevleri, kordonlar arasında taşıma işlevini güncellememiz gerekiyor. Bellek içi dizilerimizi sessize almak yerine, veritabanındaki verileri güncellemek için Firebase SDK'yı kullanacağız.

Öncelikle, yeniden sıralamanın nasıl görüneceğine bakalım. src/app/app.component.ts öğesindeki drop yöntemini şunlarla değiştir:

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

Yukarıdaki snippet'te yeni kod vurgulanmaktadır. Geçerli görev şeridinden hedef ortama taşımak için görevi ilk koleksiyondan kaldırıp ikincisine ekleyeceğiz. Biri gibi görünmesini istediğimiz iki işlem gerçekleştirdiğimiz için (işlemi atomik hale getirirsek) bunları bir Firestore işleminde çalıştırırız.

Şimdi de editTask yöntemini Firestore kullanmak için güncelleyelim. Kapat iletişim kutusu işleyicisinin içinde, aşağıdaki kod satırlarını değiştirmemiz gerekir:

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'yı kullanarak gerçekleştirdiğimiz bir görevle ilişkili hedef dokümana erişir ve onu siler ya da güncelleriz.

Son olarak, yeni görevler oluşturma yöntemini güncellememiz gerekiyor. this.todo.push('task') ifadesini this.store.collection('todo').add(result.task) ile değiştir.

Koleksiyonlarımızın artık dizi değil, gözlemlenebilir olduğunu unutmayın. Bunları görselleştirmek için AppComponent şablonunu güncellememiz gerekiyor. Bunun için todo, inProgress ve done mülklerinin her erişimini sırasıyla todo | async, inProgress | async ve done | async ile değiştirmeniz yeterlidir.

Eşzamansız boru, koleksiyonlarla ilişkili gözlemlenebilir alanlara otomatik olarak abone olur. Gözlemlenebilirler yeni bir değer yaydığında, Angular otomatik olarak değişiklik algılamayı çalıştırır ve yayınlanan diziyi işler.

Örneğin, todo yüzme şeridinde yapmamız gereken değişikliklere göz atalım:

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>

Verileri cdkDropList yönergesine ilettiğimizde eş zamansız kanalı kullanırız. *ngIf yönergesinde aynıdır ancak burada, todo | async null veya undefined değilse bir çalışma zamanı hatası almadığımızdan emin olmak için length özelliğine erişirken isteğe bağlı zincirleme (Angular'da güvenli gezinme operatörü olarak da bilinir) kullandığımızı da unutmayın.

Artık kullanıcı arayüzünde yeni bir görev oluşturup Firestore'u açtığınızda şuna benzer bir sonuç göreceksiniz:

gg7ee20c0a10ebe2.png

12. İyimser güncellemeleri iyileştirme

Başvuruda şu anda optimum güncellemeler yapıyoruz. Firestore'da gerçeğe dayalı kaynağımız vardır ancak aynı zamanda görevlerin yerel kopyaları da mevcuttur. Koleksiyonlarla ilişkili gözlemlenebilir öğelerden herhangi biri yayınlandığında bir dizi görev alırız. Bir kullanıcı işlemi durumu değiştirdiğinde, öncelikle yerel değerleri günceller ve ardından değişikliği Firestore'a yayarız.

Bir görevi bir köprüden diğerine taşırken her köprüdeki görevleri temsil eden dizilerin yerel örneklerinde çalışan transferArrayItem, çağrısını yaparız. Firebase SDK'sı bu dizileri sabit olarak kabul eder. Yani Angular'ın değişiklik algılamayı tekrar çalıştırdığı durumlarda bu örneklerin yeni örnekleri alınır. Bu, görev aktarılmadan önce önceki durumu oluşturur.

Aynı zamanda, bir Firestore güncellemesini tetikleriz ve Firebase SDK'sı doğru değerlere sahip bir güncellemeyi tetikler. Böylece, birkaç milisaniye içinde kullanıcı arayüzü doğru duruma gelir. Bu sayede, az önce aktardığımız görev ilk listeden bir sonraki listeye geçer. Bunu aşağıdaki GIF'te görebilirsiniz:

70b946eebfa6f316.gif

Bu sorunu çözmenin doğru yolu, uygulamadan uygulamaya değişir ancak verilerimiz güncellenene kadar tutarlı durumu korumamız gerekir.

valueChanges ürününden aldığımız orijinal gözlemciyi sarmalayan BehaviorSubject'ten yararlanabiliriz. Kapanışta, BehaviorSubject güncellemeyi transferArrayItem adlı kullanıcıdan tutan değiştirilebilir bir dizide kalıyor.

Bir düzeltmeyi uygulamak için tek yapmamız gereken AppComponent uygulamasını güncellemektir:

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

Yukarıdaki snippet'te yaptığımız şey, koleksiyonla ilişkili gözlemlenebilir her değişiklik olduğunda bir değer yayan bir BehaviorSubject oluşturmaktır.

BehaviorSubject, diziyi değişiklik algılama çağrılarında yeniden kullandığından ve yalnızca Firestore'dan yeni bir değer aldığımızda güncellendiğinden her şey beklendiği gibi çalışır.

13. Uygulamanın dağıtılması

Uygulamamızı dağıtmak için yapmamız gereken tek şey:

ng deploy

Bu komut:

  1. Derleme zamanı optimizasyonları uygulayarak uygulamanızı üretim yapılandırmasıyla oluşturun.
  2. Uygulamanızı Firebase Hosting'e dağıtın.
  3. Sonucu önizlemek için URL çıkışı yapın.

14. Tebrikler

Tebrikler, Angular ve Firebase ile başarılı bir kanban panosu oluşturdunuz.

Farklı görevlerin durumunu gösteren üç sütunlu bir kullanıcı arayüzü oluşturdunuz. Angular CDK'yi kullanarak, sütunlar arasında görevleri sürükleyip bırakma işlemini uyguladınız. Ardından, Angular materyalini kullanarak yeni görevler oluşturmak ve mevcut görevleri düzenlemek için bir form oluşturdunuz. Ardından, @angular/fire ürününü nasıl kullanacağınızı öğrendiniz ve tüm uygulama durumunu Firestore'a taşıdınız. Son olarak, uygulamanızı Firebase Hosting'e dağıttınız.

Sırada ne var?

Uygulamayı test yapılandırmaları kullanarak dağıttığımızı unutmayın. Uygulamanızı üretim kanalına dağıtmadan önce doğru izinleri ayarladığınızdan emin olun. Bunu nasıl yapacağınızı buradan öğrenebilirsiniz.

Şu anda, tek bir yüzme şeridindeki görevlerin sırasını korumuyoruz. Bunu uygulamak için görev dokümanında bir sipariş alanını kullanabilir ve buna göre sıralama yapabilirsiniz.

Ek olarak, kanban panosunu yalnızca tek bir kullanıcı için oluşturduk. Bu da, uygulamayı açan herkes için tek bir kanban panosunun bulunduğu anlamına geliyor. Uygulamanızın farklı kullanıcılarına yönelik ayrı panolar uygulamak için veritabanı yapınızı değiştirmeniz gerekecek. Firestore'un en iyi uygulamaları hakkında buradan bilgi edinin.