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.
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:
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:
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:
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şicdkDropListConnectedTo
: MevcutcdkDropList
cihazının bağlı olduğu diğercdkDropList
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 paragrafapp-task
bileşeni. Burada,editTask
yöntemini listenin adı ve$event
nesnesiyle çağırarak başlangıçta bildirdiğimizedit
çı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 gibitodo
listesini yineler vetask
girişini iletiriz. Ancak bu kezcdkDrag
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:
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örevitodo
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:
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:
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:
"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şaretitrue
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.
- Firebase Console'a gidin.
- "KanbanFire" adlı yeni bir proje oluşturun.
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:
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.
Ardından, test modunda bir veritabanı oluşturun:
Son olarak bir bölge seçin:
Ş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.
"Uygulamalarınız" bölümünde bir "Web uygulaması" seçin:
Ardından, uygulamanızı kaydedin ve "Firebase Hosting"i etkinleştirdiğinizden emin olun:
"Uygulamayı kaydet"i tıkladıktan sonra yapılandırmanızı src/environments/environment.ts
içine 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'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:
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:
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:
- Derleme zamanı optimizasyonlarını uygulayarak uygulamanızı üretim yapılandırmasıyla oluşturun.
- Uygulamanızı Firebase Hosting'e dağıtın.
- 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.