Angular ve Firebase ile web uygulaması oluşturma

1. Giriş

Last Updated: 2020-09-11

Ne oluşturacaksınız?

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, Angular KSA'yı kullanarak uygulamayı Firebase Hosting'e dağıtacağız.

b23bd3732d0206b.png

Neler öğreneceksiniz?

  • Angular materyali ve CDK'yi kullanma.
  • Angular uygulamanıza Firebase entegrasyonu 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ğu ve Angular ile Angular CLI hakkında temel bilgiye sahip olduğunuz varsayılır.

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 CLI, 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ın geliştirme sunucusunu başlatın:

ng serve

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

5ede7bc5b1109bf3.png

Düzenleyicinizde src/app/app.component.html dosyasını açın ve tüm içeriğini silin. http://localhost:4200 adresine geri döndüğünüzde boş bir sayfa görmelisiniz.

3. Material ve CDK'yı ekleme

Angular, @angular/material paketi kapsamında Material Design'a uygun kullanıcı arayüzü bileşenlerinin uygulanmasıyla birlikte gelir. @angular/material'nın bağımlılıklarından biri Component Development Kit (Bileşen Geliştirme Kiti) veya CDK'dır. CDK; a11y yardımcı programları, sürükle ve bırakma ve yer paylaşımı gibi temel öğeler sağlar. CDK'yı @angular/cdk paketinde dağıtıyoruz.

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

ng add @angular/material

Bu komut, genel materyal tipografi stillerini kullanmak ve Angular Material için tarayıcı animasyonlarını ayarlamak isteyip istemediğinizi sorar. Bu codelab'dekiyle aynı sonucu almak için "Indigo/Pink"i seçin ve son iki soruyu "Evet" olarak yanıtlayın.

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

İlk olarak AppComponent öğesine bir araç çubuğu ve simge ekleyelim. app.component.html bağlantısını 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, Materyal Tasarım temamızın birincil rengini kullanarak bir araç çubuğu ekliyoruz ve bu araç çubuğunun içinde "Kanban Fire" etiketinin yanındaki local_fire_depeartment simgesini kullanıyoruz. Şimdi konsolunuza bakarsanız Angular'ın birkaç hata verdiğini görürsünüz. Bu sorunları düzeltmek için AppModule dosyasına aşağıdaki içe aktarma işlemlerini 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 { }

Angular materyal araç çubuğu ve simgesi kullandığımız için ilgili modülleri AppModule içine aktarmamız gerekir.

Ekranda aşağıdaki bilgileri görmeniz gerekir:

a39cf8f8428a03bc.png

Yalnızca 4 satır HTML ve iki içe aktarma ile fena değil!

4. Görevleri görselleştirme

Bir 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 KSA komutunu çalıştırın:

ng generate component task

Bu komut, TaskComponent oluşturur ve bildirimini AppModule öğesine ekler. task dizininde task.ts adlı bir dosya oluşturun. Bu dosyayı, kanban panosundaki görevlerin arayüzünü tanımlamak için kullanırız. Her görevde isteğe bağlı olarak id, title ve description alanları bulunur. Bu alanların tümü dize türündedir:

src/app/task/task.ts

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

Şimdi task.component.ts uygulamasını güncelleyelim. TaskComponent işlevinin, Task türünde bir nesneyi giriş olarak kabul etmesini ve "edit" çıkışlarını verebilmesini istiyoruz:

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 şablonunu düzenleyin. task.component.html dosyasını açın ve 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 hatalar gösterildiğini fark edebilirsiniz:

'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 içindeki mat-card bileşenini kullanıyoruz ancak karşılık gelen modülünü uygulamaya 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 { }

Ardından, AppComponent bölümünde birkaç görev oluşturup TaskComponent kullanarak bunları görselleştireceğiz.

AppComponent içinde 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 ifadesinin en altı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 aşağıdakileri görmeniz gerekir:

d96fccd13c63ceb1.png

5. Görevler için sürükleyip bırakma özelliğini uygulama

Şimdi eğlenceli kısma geçmeye hazırız. Görevlerin bulunabileceği üç farklı durum için üç şerit oluşturalım ve Angular CDK'yı kullanarak sürükle ve bırak işlevini uygulayalım.

app.component.html içinde, üstte *ngFor yönergesi bulunan app-task bileşenini kaldırın ve aşağıdakiyle değiştirin:

src/app/app.component.html

<div class="content-wrapper">
  <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>
</div>

Burada çok şey oluyor. Şimdi bu snippet'in her bir bölümüne adım adım göz atalım. Şablonun üst düzey yapısı şöyledir:

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, sınıf adı "container-wrapper" olan ve üç şeridin tamamını kapsayan bir div oluşturuyoruz. Her şeridin sınıf adı "container" ve h2 etiketi içinde bir başlığı var.

Şimdi ilk şeridin 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, mat-card olarak tanımladığımız şeridi cdkDropList yönergesiyle kullanırız. Bu bileşenin sağladığı stiller nedeniyle mat-card kullanıyoruz. cdkDropList, daha sonra öğenin içine görev bırakmamıza olanak tanır. Ayrıca aşağıdaki iki girişi de ayarladık:

  • cdkDropListData - Veri dizisini belirtmemize olanak tanıyan açılır listenin girişi
  • cdkDropListConnectedTo: Mevcut cdkDropList cihazının bağlı olduğu diğer cdkDropList cihazlarına yapılan referanslar. Bu girişi ayarlayarak öğeleri hangi diğer listelere bırakabileceğimizi belirtiriz.

Ayrıca, bırakma etkinliğini cdkDropListDropped çıkışını kullanarak işlemek istiyoruz. cdkDropList bu çıkışı verdiğinde AppComponent içinde tanımlanan drop yöntemini çağırıp mevcut etkinliği bağımsız değişken olarak ileteceğiz.

Bu kapsayıcı için tanımlayıcı olarak kullanılacak bir id ve stil uygulayabilmemiz için bir class adı da belirttiğimizi unutmayın. Şimdi de mat-card öğesinin alt öğelerine bakalım. Burada iki öğe bulunur:

  • todo listesinde öğe olmadığında "Boş liste" metnini göstermek için kullandığımız bir paragraf
  • app-task bileşeni. Burada, editTask yöntemini listenin adı ve $event nesnesiyle çağırarak başlangıçta bildirdiğimiz edit çıkışını işlediğimizi unutmayın. Bu sayede, düzenlenen görevi doğru listede değiştirebiliriz. Ardından, yukarıda yaptığımız gibi todo listesini yineler ve task girişini iletiriz. Ancak bu kez cdkDrag yönergesini de ekliyoruz. Görevleri tek tek sürüklenebilir hale getirir.

Tüm bunların çalışması için app.module.ts dosyasını güncellememiz ve DragDropModule dosyasına 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 bildirmemiz 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[]>): 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, önce görevin geldiği listeyle aynı listeye bırakıp bırakmadığımızı kontrol ettiğimizi unutmayın. Bu durumda hemen geri döneriz. Aksi takdirde, mevcut görevi hedef şeride aktarırız.

Sonuç şu şekilde olmalıdır:

460f86bcd10454cf.png

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

6. Yeni görevler oluşturma

Şimdi yeni görevler oluşturma işlevini uygulayalım. Bu amaçla, AppComponent şablonunu güncelleyelim:

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 öğesinin etrafında üst düzey bir div öğesi oluştururuz ve "Görev Ekle" etiketinin yanına "add" Materyal Tasarım simgesi içeren bir düğme ekleriz. Düğmeyi, daha sonra flexbox kullanarak yan yana yerleştireceğimiz iş akışı şeritleri listesinin üstüne yerleştirmek için ekstra sarmalayıcıya ihtiyacımız var. Bu düğme, materyal düğmesi bileşenini kullandığından AppModule bölümünde ilgili modülü içe aktarmamız gerekir:

src/app/app.module.ts

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

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

Şimdi de AppComponent içinde görev ekleme işlevini uygulayalım. Materyal iletişim kutusu kullanacağız. İletişim kutusunda iki alanlı bir form bulunur: başlık ve açıklama. Kullanıcı "Görev Ekle" düğmesini tıkladığında iletişim kutusunu açacağız ve kullanıcı formu gönderdiğinde yeni oluşturulan görevi todo listesine ekleyeceğiz.

Bu işlevin AppComponent genel olarak nasıl uygulandığına bakalı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şturucu bildiririz. newTask içinde:

  • Biraz sonra tanımlayacağımız TaskDialogComponent 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 alabiliriz.
  • Kapatma etkinliğine abone oluruz ve result nesnesindeki görevi todo dizisine ekleriz.

Bu işlemin çalışması için öncelikle MatDialogModule değerini AppModule içine 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 de TaskDialogComponent oluşturma işlemine geçelim. src/app dizinine gidin ve şu komutu çalıştırın:

ng generate component task-dialog

İşlevini uygulamak için önce src/app/task-dialog/task-dialog.component.html dosyasını açın ve içeriğini şu şekilde 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şturuyoruz. Kullanıcı iletişim kutusunu açtığında cdkFocusInput direktifini kullanarak title girişine otomatik olarak odaklanıyoruz.

Şablonda, bileşenin data özelliğine nasıl referans verildiğine dikkat edin. Bu, AppComponent içindeki dialog öğesinin open yöntemine ilettiğimiz data ile aynı olacaktı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, yukarıdaki şablondaki form alanlarını kullanarak değiştirdiğimiz görev olan { task: data.task } sonucunu otomatik olarak döndürürüz.

Şimdi de 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çine iletişim kutusuna bir referans yerleştiririz. Böylece iletişim kutusunu kapatabiliriz. Ayrıca MAT_DIALOG_DATA jetonuyla ilişkili sağlayıcının değerini de yerleştiririz. Bu, yukarıdaki AppComponent içinde open yöntemine ilettiğimiz veri nesnesidir. Ayrıca, veri nesnesiyle birlikte ilettiğimiz görevin bir kopyası olan özel mülk backupTask'ü de bildiririz.

Kullanıcı iptal düğmesine bastığında, this.data.task öğesinin muhtemelen değiştirilmiş özelliklerini geri yükleriz ve this.data öğesini sonuç olarak ileterek iletişim kutusunu kapatırız.

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

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

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

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

İşlevselliği hazır hale getirmeden önce yapmamız gereken son şey, AppModule içine 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 { }

Artık "Görev Ekle" düğmesini tıkladığınızda aşağıdaki kullanıcı arayüzünü görmeniz gerekir:

33bcb987fade2a87.png

7. Uygulamanın stillerini iyileştirme

Uygulamayı daha çekici hale getirmek için stillerinde küçük değişiklikler yaparak düzenini iyileştireceğiz. Swimlane'leri yan yana yerleştirmek istiyoruz. Ayrıca "Görev Ekle" düğmesinde ve boş liste etiketinde küçük düzenlemeler yapılmasını istiyoruz.

src/app/app.component.css bağlantısını açın ve aşağıdaki stilleri en 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 ayarlıyoruz. Ayrıca, genişliğini 1400px, kenar boşluğunu ise auto olarak ayarlayarak içeriğin yatay olarak hizalanmasını sağlarız. Ardından, esnek kutu kullanarak görev şeritlerini yan yana yerleştiririz ve son olarak görevleri ve boş listeleri görselleştirme şeklimizde bazı ayarlamalar yaparız.

Uygulamanız yeniden yüklendikten sonra aşağıdaki kullanıcı arayüzünü görürsünüz:

69225f0b1aa5cb50.png

Uygulamamızın stillerini önemli ölçüde iyileştirmemize rağmen, görevleri taşıdığımızda hala can sıkıcı bir sorunla karşılaşıyoruz:

f9aae712027624af.png

"Süt al" görevini sürüklemeye başladığımızda aynı görev için iki kart görüyoruz: sürüklediğimiz kart ve swimlane'deki kart. Angular CDK, bu sorunu düzeltmek için kullanabileceğimiz CSS sınıf adları sağlar.

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

src/app/app.component.css

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

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

Bir öğeyi sürüklerken Angular CDK'nın sürükle ve bırak özelliği, öğeyi klonlayıp orijinal öğeyi bırakacağımız konuma ekler. Bu öğenin görünür olmaması için CDK'nın yer tutucuya ekleyeceği cdk-drag-placeholder sınıfında opaklık özelliğini ayarlıyoruz.

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

Ayrıca görevlerimizin stillerinde küçük düzenlemeler yapmak istiyoruz. task.component.css içinde ana makine öğesinin görüntüleme özelliğini block olarak ayarlayalım ve bazı kenar boşlukları 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 daha önce uyguladığımız işlevlerin çoğunu yeniden kullanacağız. Kullanıcı bir görevi çift tıkladığında TaskDialogComponent açılır ve formdaki iki alan görevin title ve description ile doldurulur.

TaskDialogComponent bölümüne silme düğmesi de ekleyeceğiz. Kullanıcı bu düğmeyi tıkladığında, AppComponent ile sonuçlanacak bir silme talimatı iletiriz.

TaskDialogComponent içinde yapmamız gereken tek değişiklik şablonunda olmalıdır:

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üğmede, materyali silme simgesi gösterilir. Kullanıcı bunu tıkladığında iletişim kutusunu kapatırız ve sonuç olarak nesne değişmezini { task: data.task, delete: true } iletiriz. Ayrıca, mat-fab kullanarak düğmeyi daire şeklinde yaptığımızı, rengini birincil olarak ayarladığımızı ve yalnızca iletişim kutusu verilerinde silme etkinleştirildiğinde gösterdiğimizi de unutmayın.

Düzenleme ve silme işlevlerinin geri kalan uygulaması AppComponent'da yer alır. 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 argümanlarına bakalım:

  • 'done' | 'todo' | 'inProgress', türünde bir liste. Bu tür, her bir kulvarla ilişkili özelliklere karşılık gelen değerlere sahip bir dize değişmez birliği türüdür.
  • Düzenlemek istediğimiz mevcut görev.

Yöntemin gövdesinde önce TaskDialogComponent örneğini açarız. data olarak, düzenlemek istediğimiz görevi belirten ve enableDelete özelliğini true olarak ayarlayarak formdaki düzenleme düğmesini de etkinleştiren bir nesne değişmezi iletiyoruz.

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

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

9. Yeni bir Firebase projesi oluşturma

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

10. Projeye Firebase'i ekleme

Bu bölümde projemizi Firebase ile 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 şu komutu çalıştırın:

ng add @angular/fire

Bu komut, @angular/fire paketini yükler ve size birkaç soru sorar. Terminalinizde aşağıdakine benzer bir şey görmelisiniz:

9ba88c0d52d18d0.png

Bu sırada, yükleme işlemi bir tarayıcı penceresi açar. Böylece Firebase hesabınızla kimliğinizi doğrulayabilirsiniz. Son olarak, bir Firebase projesi seçmenizi ister ve diskinizde bazı dosyalar oluşturur.

Ardından, bir Firestore veritabanı oluşturmamız gerekiyor. "Cloud Firestore" bölümünde "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 yapmanız gereken tek şey Firebase yapılandırmasını ortamınıza eklemek. Proje yapılandırmanızı Firebase konsolunda bulabilirsiniz.

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

c8253a20031de8a9.png

"Uygulamalarınız" bölümünde bir "Web uygulaması" seçin:

428a1abcd0f90b23.png

Ardından, uygulamanızı kaydedin 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 içine 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'yı ayarladığımıza göre, verilerimizi Firestore'a taşımak için @angular/fire kullanalım. İlk olarak, AppModule içinde 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'nın oluşturucusuna AppComponent'u yerleştirmemiz gerekir:

src/app/app.component.ts

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

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

Ardından, kulvar dizilerini başlatma şeklimizi 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 kullanıyoruz. valueChanges işlevinin dizi yerine gözlemlenebilir bir değer döndürdüğünü ve bu koleksiyondaki dokümanların kimlik alanının, Task arayüzünde kullandığımız adla eşleşmesi için id olarak adlandırılması gerektiğini belirttiğimizi unutmayın. valueChanges tarafından döndürülen observable, her değiştiğinde bir görev koleksiyonu yayınlar.

Diziler yerine gözlemlenebilir öğelerle çalıştığımız için görev ekleme, kaldırma ve düzenleme yöntemimizi ve görevleri şeritler arasında taşıma işlevini güncellememiz gerekiyor. Bellek içi dizilerimizi değiştirmek yerine, veritabanındaki verileri güncellemek için Firebase SDK'sını kullanacağız.

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

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 vurgulanmıştır. Bir görevi mevcut swimlane'den hedef swimlane'e taşımak için görevi ilk koleksiyondan kaldırıp ikinci koleksiyona ekleyeceğiz. Tek bir işlem gibi görünmesini istediğimiz iki işlem gerçekleştirdiğimiz için (ör. işlemi atomik hale getirme) bunları Firestore işleminde çalıştırırız.

Ardından, Firestore'u kullanmak için editTask yöntemini güncelleyelim. Kapatma 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 üzerinde işlem yaptığımız görevle ilgili hedef belgeye erişir ve bu belgeyi siler veya güncelleriz.

Son olarak, yeni görev oluşturma yöntemini güncellememiz gerekiyor. this.todo.push('task') yerine this.store.collection('todo').add(result.task) yazın.

Koleksiyonlarımızın artık diziler değil, gözlemlenebilir öğeler olduğunu fark edin. Bunları görselleştirebilmek için AppComponent şablonunu güncellememiz gerekiyor. todo, inProgress ve done özelliklerinin her erişimini sırasıyla todo | async, inProgress | async ve done | async ile değiştirmeniz yeterlidir.

Async pipe, koleksiyonlarla ilişkili gözlemlenebilir öğelere otomatik olarak abone olur. Gözlemlenebilirler yeni bir değer yayınladığında Angular, değişiklik algılamayı otomatik olarak çalıştırır ve yayınlanan diziyi işler.

Örneğin, todo şeridinde yapmamız gereken değişikliklere bakalı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 iletirken async borusunu uygularız. Bu, *ngIf yönergesinin içinde de aynıdır ancak length özelliğine erişirken todo | async, null veya undefined değilse çalışma zamanı hatası almamak için isteğe bağlı zincirleme (Angular'da güvenli gezinme operatörü olarak da bilinir) kullandığımızı unutmayın.

Artık kullanıcı arayüzünde yeni bir görev oluşturup Firestore'u açtığınızda aşağıdakine benzer bir ekranla karşılaşmanız gerekir:

dd7ee20c0a10ebe2.png

12. İyimser güncellemeleri iyileştirme

Uygulamada şu anda iyimser güncellemeler yapıyoruz. Doğruluk kaynağımız Firestore'da bulunuyor ancak aynı zamanda görevlerin yerel kopyaları da var. Koleksiyonlarla ilişkili gözlemlenebilir öğelerden herhangi biri yayın yaptığında bir görev dizisi alıyoruz. Bir kullanıcı işlemi durumu değiştirdiğinde önce yerel değerleri güncelleriz, ardından değişikliği Firestore'a yayarız.

Bir görevi bir şeritten diğerine taşıdığımızda, her şeritteki görevleri temsil eden dizilerin yerel örneklerinde çalışan transferArrayItem, işlevini çağırırız. Firebase SDK'sı bu dizileri değişmez olarak ele alır. Bu nedenle, Angular bir sonraki değişiklik algılamayı çalıştırdığında bu dizilerin yeni örneklerini alırız. Bu da görevi aktarmadan önce önceki durumu oluşturur.

Aynı zamanda bir Firestore güncellemesi tetikleriz ve Firebase SDK, doğru değerlerle bir güncellemeyi tetikler. Böylece kullanıcı arayüzü birkaç milisaniye içinde doğru duruma gelir. Bu işlem, az önce aktardığımız görevin ilk listeden bir sonraki listeye geçmesini sağlar. Bu durumu aşağıdaki GIF'te net bir şekilde görebilirsiniz:

70b946eebfa6f316.gif

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

valueChanges'den aldığımız orijinal gözlemciyi sarmalayan BehaviorSubject'den yararlanabiliriz. Arka planda BehaviorSubject, transferArrayItem güncellemesini kalıcı hale getiren değiştirilebilir bir dizi tutar.

Düzeltmeyi uygulamak için tek yapmamız gereken AppComponent hizmetini güncellemek:

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 tek şey, koleksiyonla ilişkili gözlemlenebilir her değiştiğinde bir değer yayan BehaviorSubject oluşturmaktır.

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

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

Uygulamamızı dağıtmak için yapmamız gereken tek şey şu komutu çalıştırmaktır:

ng deploy

Bu komut:

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

14. Tebrikler

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

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

Yapabilecekleriniz

Uygulamayı test yapılandırmalarını kullanarak dağıttığımızı unutmayın. Uygulamanızı üretime dağıtmadan önce doğru izinleri ayarladığınızdan emin olun. Bu işlemi nasıl yapacağınızı buradan öğrenebilirsiniz.

Şu anda belirli bir şeritteki görevlerin sırasını korumuyoruz. Bunu uygulamak için görev dokümanında bir sıra alanı kullanabilir ve bu alana göre sıralama yapabilirsiniz.

Ayrıca, kanban panosunu yalnızca tek bir kullanıcı için oluşturduk. Bu nedenle, uygulamayı açan herkes için tek bir kanban panosu vardır. Uygulamanızın farklı kullanıcıları için ayrı panolar uygulamak istiyorsanız veritabanı yapınızı değiştirmeniz gerekir. Firestore'un en iyi uygulamaları hakkında bilgi edinmek için burayı inceleyin.