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.
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:
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:
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:
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
"" 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şicdkDropListConnectedTo
- geçerlicdkDropList
öğesinin bağlı olduğu diğercdkDropList
'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ırapp-task
bileşeni. Burada, liste adını ve$event
nesnesini içereneditTask
yöntemini çağırarak başlangıçta belirttiğimizedit
çı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 gibitodo
listesini tekrarlayıptask
girişini geçiririz. Ancak bu kezcdkDrag
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:
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örevitodo
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:
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:
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:
"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''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şaretitrue
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.
- Firebase Konsolu'na gidin.
- "KanbanFire" adlı yeni bir proje oluşturun.
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:
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.
Ardından, test modunda bir veritabanı oluşturun:
Son olarak, bir bölge seçin:
Ş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.
"Uygulamanız" bölümünün altında "Web uygulaması"nı seçin:
Ardından, uygulamanızı kaydettirin ve "Firebase Hosting"i etkinleştirdiğinizden emin olun:
"Uygulamayı kaydet"i tıkladıktan sonra yapılandırmanızı src/environments/environment.ts
ürününe kopyalayabilirsiniz:
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:
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:
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:
- Derleme zamanı optimizasyonları uygulayarak uygulamanızı üretim yapılandırmasıyla oluşturun.
- Uygulamanızı Firebase Hosting'e dağıtın.
- 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.