1. Introdução
Última atualização: 11/09/2020
O que você vai criar
Neste codelab, vamos criar um quadro Kanban na Web com o Angular e o Firebase. Nosso app final vai ter três categorias de tarefas: backlog, em andamento e concluída. Poderemos criar, excluir e transferir tarefas de uma categoria para outra usando o recurso de arrastar e soltar.
Vamos desenvolver a interface do usuário usando o Angular e usar o Firestore como armazenamento permanente. Ao final do codelab, vamos implantar o app no Firebase Hosting usando a CLI do Angular.
O que você vai aprender
- Como usar o material do Angular e o CDK.
- Como adicionar a integração do Firebase ao app do Angular.
- Como manter os dados permanentes no Firestore.
- Como implantar o app no Firebase Hosting usando a CLI do Angular com um único comando.
O que é necessário
Neste codelab, presumimos que você tenha uma Conta do Google e uma compreensão básica do Angular e da CLI do Angular.
Vamos começar.
2. Como criar um novo projeto
Primeiro, vamos criar um novo espaço de trabalho do Angular:
ng new kanban-fire
? Would you like to add Angular routing? No
? Which stylesheet format would you like to use? CSS
Esta etapa pode levar alguns minutos. A CLI do Angular cria a estrutura do projeto e instala todas as dependências. Quando o processo de instalação terminar, acesse o diretório kanban-fire
e inicie o servidor de desenvolvimento da CLI do Angular:
ng serve
Abra http://localhost:4200. Uma saída parecida com esta será mostrada:
No editor, abra src/app/app.component.html
e exclua todo o conteúdo. Quando você voltar para http://localhost:4200, vai ver uma página em branco.
3. Como adicionar o Material Design e o CDK
O Angular vem com uma implementação de componentes da interface do usuário compatíveis com o Material Design como parte do pacote @angular/material
. Uma das dependências do @angular/material
é o Kit de desenvolvimento de componentes ou CDK, na sigla em inglês. O CDK oferece elementos primitivos, como utilitários de acessibilidade, recursos de arrastar e soltar e sobreposição. Distribuímos o CDK no pacote @angular/cdk
.
Para adicionar o Material Design à execução do app:
ng add @angular/material
Esse comando pede que você escolha um tema se quiser usar os estilos de tipografia globais do Material Design e configurar as animações do navegador para o Material Design do Angular. Escolha "Indigo/Pink" para gerar o mesmo resultado deste codelab e responda "Yes" (Sim) para as duas últimas perguntas.
O comando ng add
instala o @angular/material
, as dependências dele e importa o BrowserAnimationsModule
para o AppModule
. Na próxima etapa, vamos começar a usar os componentes oferecidos por este módulo.
Primeiro, vamos adicionar uma barra de ferramentas e um ícone ao AppComponent
. Abra app.component.html
e adicione a seguinte marcação:
src/app/app.component.html
<mat-toolbar color="primary">
<mat-icon>local_fire_department</mat-icon>
<span>Kanban Fire</span>
</mat-toolbar>
Aqui, adicionamos uma barra de ferramentas usando a cor principal do tema do Material Design. Dentro dela, usamos o ícone local_fire_depeartment
ao lado do rótulo "Kanban Fire". Se você observar o console agora, vai ver que o Angular gera alguns erros. Para corrigir os erros, adicione as seguintes importações ao AppModule
:
src/app/app.module.ts
...
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatIconModule } from '@angular/material/icon';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
MatToolbarModule,
MatIconModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Como usamos a barra de ferramentas e o ícone do Material Design do Angular, precisamos importar os módulos correspondentes para o AppModule
.
Você vai ver o seguinte na tela:
Nada mal com apenas quatro linhas de HTML e duas importações.
4. Como visualizar tarefas
Na próxima etapa, vamos criar um componente que pode ser usado para visualizar as tarefas no quadro Kanban.
Acesse o diretório src/app
e execute o seguinte comando da CLI:
ng generate component task
Esse comando gera o TaskComponent
e adiciona a declaração ao AppModule
. No diretório task
, crie um arquivo chamado task.ts
. Vamos usar esse arquivo para definir a interface das tarefas no quadro Kanban. Cada tarefa terá os campos opcionais id
, title
e description
, todos do tipo string:
src/app/task/task.ts
export interface Task {
id?: string;
title: string;
description: string;
}
Agora, vamos atualizar task.component.ts
. Queremos que o TaskComponent
aceite como entrada um objeto do tipo Task
e que emita as saídas "edit
":
src/app/task/task.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { Task } from './task';
@Component({
selector: 'app-task',
templateUrl: './task.component.html',
styleUrls: ['./task.component.css']
})
export class TaskComponent {
@Input() task: Task | null = null;
@Output() edit = new EventEmitter<Task>();
}
Edite o modelo do TaskComponent
. Abra task.component.html
e substitua o conteúdo pelo seguinte HTML:
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>
Observe que agora há erros no console:
'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
No modelo acima, usamos o componente mat-card
do @angular/material
, mas não importamos o módulo correspondente para o app. Para corrigir o erro acima, precisamos importar o MatCardModule
para o AppModule
:
src/app/app.module.ts
...
import { MatCardModule } from '@angular/material/card';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
MatCardModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Em seguida, vamos criar algumas tarefas no AppComponent
, que podem ser visualizadas usando o TaskComponent
.
No AppComponent
, defina uma matriz chamada todo
. Dentro dela, adicione duas tarefas:
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!'
}
];
}
Agora, na parte inferior de app.component.html
, adicione a seguinte diretiva *ngFor
:
src/app/app.component.html
<app-task *ngFor="let task of todo" [task]="task"></app-task>
Ao abrir o navegador, você verá o seguinte:
5. Como implementar o recurso de arrastar e soltar tarefas
Chegou a hora da diversão! Vamos criar três raias para os três estados em que as tarefas podem estar. Também vamos implementar uma função de arrastar e soltar usando o CDK do Angular.
Em app.component.html
, remova o componente app-task
com a diretiva *ngFor
na parte superior e substitua por:
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>
Muita coisa está acontecendo aqui. Vamos dar uma olhada em cada parte do snippet. Esta é a estrutura de nível superior do modelo:
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>
Aqui, criamos um div
que une as três raias, com o nome de classe "container-wrapper
". Cada raia tem um nome de classe "container
" e um título dentro de uma tag h2
.
Agora vamos analisar a estrutura da primeira raia:
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>
...
Primeiro, definimos a raia como um mat-card
, que usa a diretiva cdkDropList
. Usamos um mat-card
devido aos estilos que esse componente oferece. Mais tarde, o cdkDropList
permitirá soltar tarefas dentro do elemento. Também definimos as duas entradas a seguir:
cdkDropListData
: entrada da lista suspensa que permite especificar a matriz de dadoscdkDropListConnectedTo
: referências às outrascdkDropList
s às quais acdkDropList
atual está conectada. Ao configurar essa entrada, especificamos em quais outras listas podemos soltar os itens
Além disso, queremos processar o evento de soltar usando a saída cdkDropListDropped
. Depois que a cdkDropList
emitir essa saída, invocaremos o método drop
declarado no AppComponent
e transmitiremos o evento atual como um argumento.
Também especificamos um id
a ser usado como identificador desse contêiner e um nome de class
para definir um estilo para ele. Agora vamos analisar o conteúdo filho do mat-card
. Temos estes dois elementos:
- Um parágrafo que usamos para mostrar o texto "Lista vazia" quando não há itens na lista
todo
. - O componente
app-task
. Processamos a saídaedit
que declaramos originalmente chamando o métodoeditTask
com o nome da lista e o objeto$event
. Isso nos ajuda a substituir a tarefa editada na lista correta. Em seguida, iteramos a listatodo
como fizemos acima e transmitimos a entradatask
. No entanto, desta vez, também adicionamos a diretivacdkDrag
. Ela torna as tarefas arrastáveis.
Para que tudo isso funcione, precisamos atualizar o app.module.ts
e incluir uma importação para o DragDropModule
:
src/app/app.module.ts
...
import { DragDropModule } from '@angular/cdk/drag-drop';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
DragDropModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Também precisamos declarar as matrizes inProgress
e done
, junto com os métodos editTask
e drop
:
src/app/app.component.ts
...
import { CdkDragDrop, transferArrayItem } from '@angular/cdk/drag-drop';
@Component(...)
export class AppComponent {
todo: Task[] = [...];
inProgress: Task[] = [];
done: Task[] = [];
editTask(list: string, task: Task): void {}
drop(event: CdkDragDrop<Task[]|null>): void {
if (event.previousContainer === event.container) {
return;
}
if (!event.container.data || !event.previousContainer.data) {
return;
}
transferArrayItem(
event.previousContainer.data,
event.container.data,
event.previousIndex,
event.currentIndex
);
}
}
No método drop
, primeiro conferimos se estamos soltando na mesma lista da tarefa de origem. Se for o caso, vamos retornar imediatamente. Caso contrário, a tarefa atual será transferida para a raia de destino.
O resultado será:
Neste ponto, você já pode transferir itens entre as duas listas.
6. Como criar novas tarefas
Agora vamos implementar uma função para criar novas tarefas. Para isso, vamos atualizar o modelo do AppComponent
:
src/app/app.component.html
<mat-toolbar color="primary">
...
</mat-toolbar>
<div class="content-wrapper">
<button (click)="newTask()" mat-button>
<mat-icon>add</mat-icon> Add Task
</button>
<div class="container-wrapper">
<div class="container">
...
</div>
</div>
Criamos um elemento div
de nível superior ao redor do container-wrapper
e adicionamos um botão com um ícone do material "add
" ao lado do rótulo "Add Task" (Adicionar tarefa). Precisamos do wrapper extra para posicionar o botão na parte superior da lista de raias. Depois, colocaremos um ao lado do outro usando a flexbox. Como esse botão usa o componente do Material Design, precisamos importar o módulo correspondente para o AppModule
:
src/app/app.module.ts
...
import { MatButtonModule } from '@angular/material/button';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
MatButtonModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Agora, vamos implementar a função para adicionar tarefas ao AppComponent
. Usaremos uma caixa de diálogo do Material Design. Nela, há um formulário com dois campos: título e descrição. Quando o usuário clica no botão "Add Task" (Adicionar tarefa), a caixa de diálogo é aberta. Quando ele envia o formulário, adicionamos a tarefa recém-criada à lista todo
.
Vamos ver a implementação de alto nível dessa funcionalidade no AppComponent
:
src/app/app.component.ts
...
import { MatDialog } from '@angular/material/dialog';
@Component(...)
export class AppComponent {
...
constructor(private dialog: MatDialog) {}
newTask(): void {
const dialogRef = this.dialog.open(TaskDialogComponent, {
width: '270px',
data: {
task: {},
},
});
dialogRef
.afterClosed()
.subscribe((result: TaskDialogResult|undefined) => {
if (!result) {
return;
}
this.todo.push(result.task);
});
}
}
Declaramos um construtor em que injetamos a classe MatDialog
. Dentro da newTask
:
- Abrimos uma nova caixa de diálogo usando o
TaskDialogComponent
, que será definido em breve. - Especificamos que queremos que a caixa de diálogo tenha
270px.
de largura. - Transmitimos uma tarefa vazia para a caixa de diálogo na forma de dados. No
TaskDialogComponent
, é possível gerar uma referência a esse objeto de dados. - Fazemos a inscrição no evento de encerramento e adicionamos a tarefa do objeto
result
à matriztodo
.
Para garantir o funcionamento dessas etapas, primeiro precisamos importar o MatDialogModule
para o AppModule
:
src/app/app.module.ts
...
import { MatDialogModule } from '@angular/material/dialog';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
MatDialogModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Agora, vamos criar o TaskDialogComponent
. Navegue até o diretório src/app
e execute:
ng generate component task-dialog
Para implementar a funcionalidade dele, primeiro abra src/app/task-dialog/task-dialog.component.html
e substitua o conteúdo por:
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>
No modelo acima, criamos um formulário com dois campos para title
e description
. Usamos a diretiva cdkFocusInput
para focar automaticamente a entrada title
quando o usuário abrir a caixa de diálogo.
Dentro do modelo, mencionamos a propriedade data
do componente. Ela será o mesmo data
transmitido ao método open
da dialog
no AppComponent
. Para atualizar o título e a descrição da tarefa quando o usuário mudar o conteúdo dos campos correspondentes, usamos a vinculação de dados bidirecional com ngModel
.
Quando o usuário clica no botão OK, retornamos automaticamente o resultado { task: data.task }
, que é a tarefa que mudamos usando os campos do formulário no modelo acima.
Agora, vamos implementar o controlador do componente:
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);
}
}
No TaskDialogComponent
, injetamos uma referência à caixa de diálogo para fechá-la, bem como o valor do provedor associado ao token MAT_DIALOG_DATA
. Esse é o objeto de dados que transmitimos ao método aberto no AppComponent
acima. Também declaramos a propriedade particular backupTask
, que é uma cópia da tarefa transmitida com o objeto de dados.
Quando o usuário pressiona o botão para cancelar, restauramos as propriedades que podem ter sido mudadas de this.data.task
e fechamos a caixa de diálogo, transmitindo this.data
como resultado.
Há dois tipos que mencionamos, mas não declaramos ainda: TaskDialogData
e TaskDialogResult
. Dentro de src/app/task-dialog/task-dialog.component.ts
, adicione as seguintes declarações na parte inferior do arquivo:
src/app/task-dialog/task-dialog.component.ts
...
export interface TaskDialogData {
task: Partial<Task>;
enableDelete: boolean;
}
export interface TaskDialogResult {
task: Task;
delete?: boolean;
}
A última coisa que precisamos fazer para que a funcionalidade fique pronta é importar alguns módulos para o AppModule
.
src/app/app.module.ts
...
import { MatInputModule } from '@angular/material/input';
import { FormsModule } from '@angular/forms';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
MatInputModule,
FormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Ao clicar no botão "Add Task" (Adicionar tarefa), você verá a seguinte interface do usuário:
7. Como melhorar os estilos do app
Para tornar o visual do app mais atrativo, vamos melhorar o layout fazendo ajustes nos estilos. Queremos posicionar as raias uma ao lado da outra. Também queremos fazer pequenos ajustes no botão "Add Task" (Adicionar tarefa) e no rótulo da lista vazia.
Abra src/app/app.component.css
e adicione os seguintes estilos à parte inferior:
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;
}
No snippet acima, ajustamos o layout da barra de ferramentas e do rótulo dela. Para o conteúdo ficar alinhado na horizontal, defina a largura como 1400px
e a margem como auto
. Em seguida, usando o flexbox, colocamos as raias uma ao lado da outra e, por fim, fazemos alguns ajustes na visualização de tarefas e listas vazias.
Depois que o app for recarregado, você verá a seguinte interface do usuário:
Já aprimoramos os estilos do app de forma significativa, mas ainda há um problema irritante quando movemos tarefas:
Quando começamos a arrastar a tarefa "Buy milk" (Comprar leite), vemos dois cards para a mesma tarefa: o que estamos arrastando e o outro na raia. O CDK do Angular fornece nomes de classes CSS que podemos usar para corrigir esse problema.
Adicione as seguintes substituições de estilo à parte inferior de src/app/app.component.css
:
src/app/app.component.css
.cdk-drag-animating {
transition: transform 250ms;
}
.cdk-drag-placeholder {
opacity: 0;
}
Enquanto arrastamos um elemento, o recurso de arrastar e soltar do CDK do Angular clona e insere esse elemento na posição em que o original será solto. Para o elemento não ficar visível, definimos a propriedade de opacidade na classe cdk-drag-placeholder
, que o CDK adiciona ao marcador de posição.
Além disso, quando soltamos um elemento, o CDK adiciona a classe cdk-drag-animating
. Para mostrar uma animação suave, em vez de ajustar o elemento diretamente, definimos uma transição com duração de 250ms
.
Também queremos fazer alguns pequenos ajustes nos estilos das tarefas. Em task.component.css
, vamos definir a exibição do elemento host como block
e definir algumas margens:
src/app/task/task.component.css
:host {
display: block;
}
.item {
margin-bottom: 10px;
cursor: pointer;
}
8. Como editar e excluir tarefas existentes
Para editar e remover as tarefas existentes, reutilizamos a maioria das funcionalidades já implementadas. Quando o usuário clica duas vezes em uma tarefa, abrimos o TaskDialogComponent
e preenchemos os dois campos do formulário com o title
e a description
da tarefa.
Também adicionamos um botão de exclusão no TaskDialogComponent
. Quando o usuário clicar nele, será transmitida uma instrução de exclusão, que termina no AppComponent
.
A única mudança que precisamos fazer no TaskDialogComponent
é no modelo:
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>
Esse botão mostra o ícone do material de exclusão. Quando o usuário clicar nele, fecharemos a caixa de diálogo e transmitiremos o literal de objeto { task: data.task, delete: true }
como resultado. Observe também que tornamos o botão circular usando mat-fab
, definimos a cor como principal e o mostramos somente quando os dados da caixa de diálogo têm a exclusão ativada.
O restante da implementação da funcionalidade de editar e excluir é feito no AppComponent
. Substitua o método editTask
pelo seguinte:
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;
}
});
}
...
}
Vamos analisar os argumentos do método editTask
:
- Uma lista do tipo
'done' | 'todo' | 'inProgress',
, que é um tipo de união de literal de string com valores correspondentes às propriedades associadas às raias. - A tarefa atual que queremos editar.
No corpo do método, primeiro abrimos uma instância do TaskDialogComponent
. Como data
, transmitimos um literal de objeto, que especifica a tarefa que queremos editar e também ativa o botão de edição no formulário ao definir a propriedade enableDelete
como true
.
Quando recebemos o resultado da caixa de diálogo, temos dois cenários:
- Quando a sinalização
delete
é definida comotrue
(ou seja, quando o usuário pressiona o botão de exclusão), removemos a tarefa da lista correspondente. - Como alternativa, podemos substituir a tarefa do índice em questão pela tarefa recebida do resultado da caixa de diálogo.
9. Como criar um novo projeto do Firebase
Agora, vamos criar um novo projeto do Firebase.
- Acesse o Console do Firebase.
- Crie um novo projeto com o nome "KanbanFire".
10. Como adicionar o Firebase ao projeto
Nesta seção, vamos integrar nosso projeto ao Firebase. A equipe do Firebase oferece o pacote @angular/fire
, que oferece integração entre as duas tecnologias. Para adicionar o suporte do Firebase ao seu app, abra o diretório raiz do espaço de trabalho e execute:
ng add @angular/fire
Esse comando instala o pacote @angular/fire
e faz algumas perguntas. No seu terminal, você verá algo parecido com isto:
Enquanto isso, a instalação abre uma janela do navegador para você fazer a autenticação com sua conta do Firebase. Por fim, você precisa escolher um projeto do Firebase e criar alguns arquivos no disco.
Em seguida, precisamos criar um banco de dados do Firestore. Em "Cloud Firestore", clique em "Create Database" (Criar banco de dados).
Depois disso, crie um banco de dados no modo de teste:
Por último, selecione uma região:
Agora só falta adicionar a configuração do Firebase ao seu ambiente. A configuração do projeto está disponível no Console do Firebase.
- Clique no ícone de engrenagem ao lado de "Project Overview" (Visão geral do projeto).
- Escolha "Project Settings" (Configurações do projeto).
Em "Your apps" (Seus apps), selecione "Web app" (App da Web):
Em seguida, registre seu aplicativo e ative o "Firebase Hosting":
Depois de clicar em "Register app" (Registrar app), você pode copiar sua configuração para src/environments/environment.ts
:
No fim, o arquivo de configuração ficará assim:
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. Como mover os dados para o Firestore
Agora que o SDK do Firebase foi configurado, vamos usar @angular/fire
para mover nossos dados para o Firestore. Primeiro, vamos importar os módulos necessários no AppModule
:
src/app/app.module.ts
...
import { environment } from 'src/environments/environment';
import { AngularFireModule } from '@angular/fire';
import { AngularFirestoreModule } from '@angular/fire/firestore';
@NgModule({
declarations: [AppComponent, TaskDialogComponent, TaskComponent],
imports: [
...
AngularFireModule.initializeApp(environment.firebase),
AngularFirestoreModule
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
Como vamos usar o Firestore, precisamos injetar AngularFirestore
no construtor do AppComponent
:
src/app/app.component.ts
...
import { AngularFirestore } from '@angular/fire/firestore';
@Component({...})
export class AppComponent {
...
constructor(private dialog: MatDialog, private store: AngularFirestore) {}
...
}
Em seguida, atualizamos a forma como inicializamos as matrizes da raia:
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[]>;
...
}
Aqui, usamos AngularFirestore
para receber o conteúdo da coleção diretamente do banco de dados. Observe que valueChanges
retorna um elemento observável (link em inglês) em vez de uma matriz e que também especificamos que o campo "id" dos documentos nessa coleção precisa ser chamado de id
para corresponder ao nome que usamos na interface da Task
. O elemento observável retornado por valueChanges
emite um conjunto de tarefas sempre que muda.
Como estamos trabalhando com observáveis em vez de matrizes, precisamos atualizar a maneira como adicionamos, removemos e editamos tarefas, além da funcionalidade de mover tarefas entre raias. Em vez de modificar as matrizes na memória, vamos usar o SDK do Firebase para atualizar os dados no banco de dados.
Primeiro, vamos analisar como seria a reordenação. Substitua o método drop
em src/app/app.component.ts
por:
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
);
}
No snippet acima, o novo código está destacado. Para mover uma tarefa da raia atual para a de destino, removemos a tarefa da primeira coleção e a adicionamos à segunda. Como realizamos duas operações como uma (ou seja, a operação é atômica), executamos essas operações em uma transação do Firestore.
Agora, vamos atualizar o método editTask
para usar o Firestore. Dentro do gerenciador de caixas de diálogo de fechamento, precisamos mudar as seguintes linhas de código:
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);
}
});
...
Acessamos o documento de destino correspondente à tarefa que manipulamos usando o SDK do Firestore e o excluímos ou atualizamos.
Por fim, precisamos atualizar o método para criar novas tarefas. Substitua this.todo.push('task')
por this.store.collection('todo').add(result.task)
.
Agora, nossas coleções não são matrizes, mas elementos observáveis. Para visualizá-los, precisamos atualizar o modelo do AppComponent
. Basta substituir todos os acessos das propriedades todo
, inProgress
e done
por todo | async
, inProgress | async
e done | async
, respectivamente.
O pipeline assíncrono (link em inglês) se inscreve automaticamente nos elementos observáveis associados às coleções. Quando os elementos observáveis emitem um novo valor, o Angular executa automaticamente a detecção de mudanças e processa a matriz emitida.
Por exemplo, vamos analisar as mudanças que precisamos fazer na raia todo
.
src/app/app.component.html
<mat-card
cdkDropList
id="todo"
#todoList="cdkDropList"
[cdkDropListData]="todo | async"
[cdkDropListConnectedTo]="[doneList, inProgressList]"
(cdkDropListDropped)="drop($event)"
class="list">
<p class="empty-label" *ngIf="(todo | async)?.length === 0">Empty list</p>
<app-task (edit)="editTask('todo', $event)" *ngFor="let task of todo | async" cdkDrag [task]="task"></app-task>
</mat-card>
Quando transmitimos os dados para a diretiva cdkDropList
, aplicamos o pipeline assíncrono. Ele é o mesmo da diretiva *ngIf
, mas nela também usamos encadeamento opcional (também conhecido como operador de navegação segura no Angular) ao acessar a propriedade length
. O objetivo disso é garantir que não haverá erro de tempo de execução se todo | async
não for null
ou undefined
.
Agora, ao criar uma nova tarefa na interface do usuário e abrir o Firestore, você verá algo parecido com isto:
12. Como melhorar as atualizações otimistas
No momento, estamos realizando atualizações otimistas. Temos nossa fonte de informações confiáveis no Firestore, mas também temos cópias locais das tarefas. Quando qualquer um dos elementos observáveis associados às coleções é emitido, recebemos uma matriz de tarefas. Quando uma ação do usuário muda o estado, primeiro atualizamos os valores locais e, depois, propagamos a mudança para o Firestore.
Quando movemos uma tarefa de uma raia a outra, invocamos transferArrayItem,
, que opera nas instâncias locais das matrizes que representam as tarefas em cada raia. O SDK do Firebase trata essas matrizes como imutáveis. Isso significa que, na próxima vez que o Angular executar a detecção de mudanças, vamos receber novas instâncias delas, o que renderizará o estado anterior antes da transferência da tarefa.
Ao mesmo tempo, acionamos uma atualização do Firestore, e o SDK do Firebase aciona uma atualização com os valores corretos. Dessa maneira, em alguns milissegundos, a interface do usuário vai chegar ao estado correto. Assim, a tarefa que acabamos de transferir vai da primeira lista para a próxima. Você pode ver bem isso no GIF abaixo:
A forma correta de resolver esse problema varia de um aplicativo para outro. No entanto, em todos os casos, precisamos garantir a consistência do estado até que nossos dados sejam atualizados.
Podemos usar o BehaviorSubject
(link em inglês), que une o observador original que recebemos de valueChanges
. Internamente, o BehaviorSubject
contém uma matriz mutável que mantém a atualização do transferArrayItem
.
Para implementar uma correção, basta atualizar o AppComponent
:
src/app/app.component.ts
...
import { AngularFirestore, AngularFirestoreCollection } from '@angular/fire/firestore';
import { BehaviorSubject } from 'rxjs';
const getObservable = (collection: AngularFirestoreCollection<Task>) => {
const subject = new BehaviorSubject<Task[]>([]);
collection.valueChanges({ idField: 'id' }).subscribe((val: Task[]) => {
subject.next(val);
});
return subject;
};
@Component(...)
export class AppComponent {
todo = getObservable(this.store.collection('todo')) as Observable<Task[]>;
inProgress = getObservable(this.store.collection('inProgress')) as Observable<Task[]>;
done = getObservable(this.store.collection('done')) as Observable<Task[]>;
...
}
No snippet acima, criamos um BehaviorSubject
, que emite um valor toda vez que o elemento observável associado à coleção muda.
Tudo funciona conforme o esperado, porque o BehaviorSubject
reutiliza a matriz nas invocações de detecção de mudanças e só é atualizado quando recebemos um novo valor do Firestore.
13. Como implantar o aplicativo
Para implantar o app, basta executar:
ng deploy
Esse comando vai:
- Criar seu app com a configuração de produção, aplicando otimizações no tempo de compilação.
- Implantar seu app no Firebase Hosting.
- Gerar um URL para que você possa visualizar o resultado.
14. Parabéns
Parabéns! Você criou um quadro Kanban com o Angular e o Firebase.
Você criou uma interface do usuário com três colunas que representam o status de tarefas. Usando o CDK do Angular, você implementou a função de arrastar e soltar tarefas nas colunas. Em seguida, usando o Material Design do Angular, você criou um formulário para criar novas tarefas e editar as atuais. Depois, você aprendeu a usar @angular/fire
e moveu todo o estado do aplicativo para o Firestore. Por fim, você implantou seu aplicativo no Firebase Hosting.
Qual é a próxima etapa?
Lembre-se de que implantamos o aplicativo usando configurações de teste. Antes de implantar o app no ambiente de produção, configure as permissões corretas. Aprenda a fazer isso aqui.
No momento, não preservamos a ordem das tarefas em determinada raia. Para implementar isso, use um campo de ordem no documento da tarefa e faça a classificação com base nele.
Além disso, criamos o quadro Kanban para apenas um usuário, o que significa que temos um único quadro para qualquer pessoa que abra o app. Para implementar quadros separados para diferentes usuários do seu app, você precisará mudar a estrutura do banco de dados. Saiba mais sobre as práticas recomendadas do Firestore aqui.