1. מבוא
עדכון אחרון: 2020-01-11
מה תיצור
במעבדה זו, נבנה לוח קאנבאן באינטרנט עם Angular and Firebase! לאפליקציה הסופית יש שלוש קטגוריות של משימות: יומן, התקדמות והשלמת הפעולה. נוכל ליצור, למחוק משימות ולהעביר אותן מקטגוריה אחת לקטגוריה אחרת באמצעות גרירה ושחרור.
אנחנו נפתח את ממשק המשתמש באמצעות Angular ומשתמשים ב-Firestore כחנות הקבועה שלנו. בסוף ה-Codelab נפרס את האפליקציה ב-Firebase Hosting באמצעות ממשק ה-CLI של Angular.
מה תלמדו
- איך משתמשים בחומר זוויתי וב-CDC.
- איך מוסיפים שילוב של Firebase לאפליקציה האנגלית.
- איך לשמור על הנתונים הקבועים שלכם ב-Firestore.
- איך לפרוס את האפליקציה שלכם ב-Firebase Hosting באמצעות פקודה יחידה (אנגלי) באמצעות פקודה אחת?
מה צריך?
בשיעור Lab זה מתבסס על ההנחה שיש לכם חשבון Google והבנה בסיסית של Angular and C Angular CLI.
קדימה, מתחילים!
2. יצירת פרויקט חדש
קודם כל, יוצרים סביבת עבודה זוויתית חדשה:
ng new kanban-fire
? Would you like to add Angular routing? No
? Which stylesheet format would you like to use? CSS
השלב הזה עשוי להימשך כמה דקות. CLI של Angular יוצר את פרויקט הפרויקט ומתקין את כל התלות. בסיום תהליך ההתקנה, יש לעבור לספרייה של kanban-fire
ולהתחיל את שרת הפיתוח של Angular CLI'
ng serve
פותחים את http://localhost:4200 ומקבלים פלט דומה ל:
בעורך, פותחים את src/app/app.component.html
ומוחקים את כל התוכן שלו. כשתעברו אל http://localhost:4200 אמור להופיע דף ריק.
3. הוספת חומר ו-CDC
Angular מגיע עם הטמעה של רכיבי עיצוב תואמים לממשק המשתמש, כחלק מהחבילה של @angular/material
. אחד מהתלויים ב-@angular/material
הוא ערכת פיתוח הרכיבים, או CDK. ה-CDK מספק מקורות מהנים, כמו כלי עזר לגרירה, גרירה ושחרור ושכבת-על. אנחנו מפיצים את ה-CDC בחבילה של @angular/cdk
.
כדי להוסיף תוכן לאפליקציה:
ng add @angular/material
פקודה זו מבקשת ממך לבחור עיצוב, אם ברצונך להשתמש בסגנונות הטיפוסיים הכלליים של החומר, ואם ברצונך להגדיר את האנימציות של דפדפן לחומרים זוויתיים. בחרו ב-"Indigo/Pink" כדי לקבל את אותה תוצאה שמופיעה ב-codelab זה, והשיבו ב-"כן" בשתי השאלות האחרונות.
הפקודה ng add
מתקינה את @angular/material
, והיא תלויה בייבוא של BrowserAnimationsModule
אל AppModule
. בשלב הבא, נוכל להתחיל להשתמש ברכיבים שהמודול הזה מציע!
תחילה יש להוסיף סרגל כלים וסמל אל AppComponent
. יש לפתוח את app.component.html
ולהוסיף את תגי העיצוב הבאים:
src/app/app.component.html
<mat-toolbar color="primary">
<mat-icon>local_fire_department</mat-icon>
<span>Kanban Fire</span>
</mat-toolbar>
כאן אנחנו מוסיפים סרגל כלים באמצעות הצבע העיקרי של עיצוב העיצוב מהותית, ובתוכו אנחנו משתמשים בסמל local_fire_depeartment
לצד התווית "Kanpan Fire." מעכשיו, אם תביטו במסוף תוכלו לראות שיש כמה שגיאות בזווית הזוויתית. כדי לפתור את הבעיות, צריך להוסיף את הייבוא הבא אל 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 { }
מאחר שאנחנו משתמשים בסרגל הכלים ובסמל של הזווית הזוויתית, אנחנו צריכים לייבא את המודולים המתאימים ב-AppModule
.
עכשיו אתם אמורים לראות את המסך הבא:
לא רע! יש רק ארבע שורות HTML ושתי ייבואים!
4. הצגה חזותית של משימות
בשלב הבא, ניצור רכיב שבו נוכל להשתמש כדי להציג את המשימות בלוח יכולות של קנקן.
עוברים אל הספרייה src/app
ומריצים את פקודת CLI הבאה:
ng generate component task
הפקודה הזו יוצרת את TaskComponent
ומוסיפה את ההצהרה שלו ל-AppModule
. בתוך הספרייה task
, יוצרים קובץ בשם task.ts
. אנחנו נשתמש בקובץ הזה כדי להגדיר את ממשק המשימות שבלוח קנקן. כל משימה תכלול שדות אופציונליים ב-id
, ב-title
וב-description
– במחרוזת מכל הסוגים:
src/app/task/task.ts
export interface Task {
id?: string;
title: string;
description: string;
}
עכשיו נעדכן את task.component.ts
. אנחנו רוצים שTaskComponent
יקבל כקלט אובייקט מסוג Task
, ואנחנו רוצים שהוא יוכל לפלוט את הפלט &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>();
}
ערוך את התבנית של TaskComponent
'! יש לפתוח את task.component.html
ולהחליף את התוכן שלו ב-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>
לתשומת ליבך, אנחנו מקבלים עכשיו שגיאות במסוף:
'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
בתבנית שלמעלה אנחנו משתמשים ברכיב mat-card
מ-@angular/material
, אבל לא ייבאת את המודול התואם שלו באפליקציה. כדי לתקן את השגיאה שלמעלה, אנחנו צריכים לייבא את MatCardModule
באמצעות AppModule
:
src/app/app.module.ts
...
import { MatCardModule } from '@angular/material/card';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
MatCardModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
בשלב הבא, ניצור מספר משימות בAppComponent
כדי להציג אותן באופן חזותי באמצעות TaskComponent
!
ב-AppComponent
מגדירים מערך שנקרא todo
ובתוךיו מוסיפים שתי משימות:
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!'
}
];
}
עכשיו, בחלק התחתון של app.component.html
, מוסיפים את ההוראה *ngFor
הבאה:
src/app/app.component.html
<app-task *ngFor="let task of todo" [task]="task"></app-task>
כשפותחים את הדפדפן, מופיעים הפרטים הבאים:
5. מיישמת גרירה ושחרור עבור משימות
אנחנו מוכנים עכשיו לחלק הכיף! ניצור שלושה בריכות שחייה בין שלוש המדינות השונות שבהן ניתן לבצע משימות. באמצעות CDK האנגלי, יש להשתמש בפונקציית הגרירה והשחרור.
ב-app.component.html
, יש להסיר את הרכיב app-task
בעזרת ההוראה *ngFor
בחלק העליון של המסך, ולהחליף אותו ב:
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>
יש כאן הרבה דברים. נבחן את החלקים השונים בקטע הקוד הזה. זהו המבנה ברמה העליונה של התבנית:
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>
כאן אנחנו יוצרים div
שמכסה את כל שלושת בריכות השחייה, עם שם הכיתה "container-wrapper
." לכל נתיב שחייה יש שם כיתה &מירכאות;container
" וכותרת בתוך תג h2
.
עכשיו נבחן את מבנה נתיב השחייה הראשון:
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>
...
תחילה, אנחנו מגדירים את נתיב השחייה כ-mat-card
, שמשתמש בהנחיה cdkDropList
. אנחנו משתמשים ב-mat-card
בגלל הסגנונות שהרכיב הזה מספק. מאוחר יותר, cdkDropList
יאפשר לנו לשחרר משימות בתוך הרכיב. הגדרנו גם את שתי הקלט הבא:
cdkDropListData
- קלט של רשימת הפריטים ברשימה שמאפשרת לנו לציין את מערך הנתוניםcdkDropListConnectedTo
- ההפניות אלcdkDropList
האחרות שאליהןcdkDropList
מחובר. הגדרת הקלט הזה מציינת את הרשימות האחרות שאליהן אנחנו יכולים לשלוח פריטים
בנוסף, אנחנו רוצים לטפל באירוע הירידה באמצעות הפלט של cdkDropListDropped
. לאחר שה-cdkDropList
פולט את הפלט הזה, נפעיל את שיטת drop
שהוצהרה בתוך AppComponent
ונעביר את האירוע הנוכחי כארגומנט.
שימו לב שאנחנו מציינים גם id
שישמש כמזהה של מאגר הזה, ושם class
כדי שנוכל לסגנן אותו. עכשיו נבחן את התוכן לילדים ב-mat-card
. שני הרכיבים שיש לנו הם:
- פסקה שבה אנחנו משתמשים כדי להציג את הטקסט ה&מירכאות;רשימה ריקה; כאשר אין פריטים ברשימה
todo
- רכיב
app-task
. לתשומת ליבך, כאן אנחנו מטפלים בפלטedit
שהודענו במקור על ידי קריאה לשיטהeditTask
עם שם הרשימה והאובייקט$event
. כך נוכל להחליף את המשימה הערוכה מהרשימה הנכונה. לאחר מכן, אנחנו חוזרים על הרשימהtodo
כפי שעשינו למעלה ומעבירים את הקלט שלtask
. הפעם, אנחנו מוסיפים גם את ההוראהcdkDrag
. הוא מאפשר לגרור משימות ספציפיות.
כדי לבצע את כל הפעולות האלה, אנחנו צריכים לעדכן את app.module.ts
ולכלול ייבוא לDragDropModule
:
src/app/app.module.ts
...
import { DragDropModule } from '@angular/cdk/drag-drop';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
DragDropModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
כמו כן, עלינו להצהיר על מערכי inProgress
ו-done
, בשילוב עם השיטות editTask
ו-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
);
}
}
שימו לב שבשיטה drop
אנחנו בודקים תחילה שאנחנו מורידים את אותה רשימה של המשימה. אם זה המצב, אנחנו מחזירים מיד. אם לא, אנחנו מעבירים את המשימה הנוכחית לנתיב היעד.
התוצאה צריכה להיות:
בשלב זה אמורה להיות לך אפשרות להעביר פריטים בין שתי הרשימות!
6. יצירת משימות חדשות
עכשיו ניישם פונקציונליות ליצירת משימות חדשות. לשם כך, נעדכן את התבנית של 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>
אנחנו יוצרים רכיב div
ברמה העליונה סביב ה-container-wrapper
ומוסיפים לחצן עם &מירכאות;add
" יחד עם תווית "הוספת משימה." אנחנו זקוקים לwrapper נוסף כדי למקם את הלחצן בחלק העליון של רשימת בריכות השחייה. אנחנו נוסיף את ה-Flexbox מאוחר יותר זה לצד זה. הלחצן הזה משתמש ברכיב הלחצן מהותי, ולכן אנחנו צריכים לייבא את המודול המתאים ב-AppModule
:
src/app/app.module.ts
...
import { MatButtonModule } from '@angular/material/button';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
MatButtonModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
עכשיו ניישם את הפונקציונליות של הוספת משימות ב-AppComponent
. אנחנו נשתמש בתיבת דו-שיח מהותית. בתיבת הדו-שיח יהיה לנו טופס עם שני שדות: כותרת ותיאור. כשהמשתמש לוחץ על הלחצן "הוספת משימה" 'אנחנו פותחים את תיבת הדו-שיח, וכשהמשתמש שולח את הטופס אנחנו מוסיפים את המשימה החדשה לרשימה todo
.
נבחן את ההטמעה העיקרית של הפונקציונליות הזו ב-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);
});
}
}
אנחנו מצהירים על קבלן שבו אנחנו מכניסים את הכיתה ל-MatDialog
. בתוך newTask
אנחנו:
- פותחים תיבת דו-שיח חדשה באמצעות השדה
TaskDialogComponent
שנגדיר עוד מעט. - יש לציין שרוחב תיבת הדו-שיח צריך להיות
270px.
- העברת משימה ריקה לתיבת הדו-שיח כנתונים. בעוד
TaskDialogComponent
נוכל לקבל הפניה לאובייקט הנתונים הזה. - אנחנו נרשמים לאירוע הקרוב ומוסיפים את המשימה מאובייקט
result
למערךtodo
.
כדי לוודא שזה עובד, קודם אנחנו צריכים לייבא את MatDialogModule
מ-AppModule
:
src/app/app.module.ts
...
import { MatDialogModule } from '@angular/material/dialog';
@NgModule({
declarations: [
AppComponent
],
imports: [
...
MatDialogModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
עכשיו יש ליצור את TaskDialogComponent
. עוברים אל הספרייה src/app
ומריצים:
ng generate component task-dialog
כדי להטמיע את הפונקציונליות שלו, תחילה יש לפתוח את src/app/task-dialog/task-dialog.component.html
ולהחליף את התוכן שלו ב:
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>
בתבנית שלמעלה, אנחנו יוצרים טופס עם שני שדות עבור title
ועבור description
. אנחנו משתמשים בהוראה cdkFocusInput
כדי למקד את הקלט title
באופן אוטומטי כשהמשתמש פותח את תיבת הדו-שיח.
שימו לב איך בתוך התבנית אנחנו מפנים לנכס data
של הרכיב. המספר יהיה זהה לdata
שנעביר לשיטה open
של dialog
בAppComponent
. כדי לעדכן את השם ואת התיאור של המשימה כשהמשתמש משנה את התוכן בשדות התואמים, אנחנו משתמשים בקישור דו-כיווני של הנתונים עם ngModel
.
כשהמשתמש לוחץ על הלחצן 'אישור', אנחנו מחזירים באופן אוטומטי את התוצאה { task: data.task }
, שהיא המשימה ששינינו באמצעות שדות הטופס בתבנית שלמעלה.
עכשיו נטמיע את הבקר של הרכיב:
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
, אנחנו מכניסים הפניה לתיבת הדו-שיח, כדי שנוכל לסגור אותה. כמו כן, אנחנו מכניסים את הערך של הספק המשויך לאסימון MAT_DIALOG_DATA
. זהו אובייקט הנתונים שהעברנו לשיטה הפתוחה ב-AppComponent
שלמעלה. אנחנו גם מצהירים על הנכס הפרטי backupTask
, שהוא עותק של המשימה שהעברנו יחד עם אובייקט הנתונים.
כאשר המשתמש לוחץ על לחצן הביטול, אנחנו משחזרים את המאפיינים שייתכן שהשתנו באמצעות this.data.task
וסוגרים את תיבת הדו-שיח, וכתוצאה מכך this.data
.
יש שני סוגים שאליהם פנינו, אבל עדיין לא הצהרה – TaskDialogData
ו-TaskDialogResult
. בתוך src/app/task-dialog/task-dialog.component.ts
, מוסיפים את ההצהרות הבאות לתחתית הקובץ:
src/app/task-dialog/task-dialog.component.ts
...
export interface TaskDialogData {
task: Partial<Task>;
enableDelete: boolean;
}
export interface TaskDialogResult {
task: Task;
delete?: boolean;
}
הדבר האחרון שאנחנו צריכים לעשות לפני שהפונקציונליות תהיה מוכנה הוא לייבא כמה מודולים ב-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 { }
כשלוחצים על הלחצן "הוספת משימה" עכשיו אתם אמורים לראות את ממשק המשתמש הבא:
7. שיפור הסגנונות של האפליקציה
כדי שהאפליקציה תהיה מעניינת יותר, נשפר קצת את הפריסה שלה. אנחנו רוצים למקם את השחייה זו לצד זו. כמו כן, אנחנו רוצים לבצע התאמות קלות בלחצן '&הוספת משימה' ו'תווית'; ובתווית הרשימה ריקה.
פותחים את src/app/app.component.css
ומוסיפים את הסגנונות הבאים למטה:
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;
}
בקטע הקוד שלמעלה, אנחנו משנים את הפריסה של סרגל הכלים ושל התווית שלו. כמו כן, אנחנו מוודאים שהתוכן מכוון לרוחב על ידי הגדרת הרוחב שלו ל-1400px
והשוליים שלו ל-auto
. בשלב הבא, השימוש ב-Flexbox מאפשר לנו למקם את נתיבי השחייה זה לצד זה, ולבסוף מבצעים התאמות מסוימות באופן שבו אנחנו מציגים באופן חזותי משימות ורשימות ריקות.
לאחר שהאפליקציה נטענת מחדש, אתם אמורים לראות את ממשק המשתמש הבא:
שיפרנו באופן משמעותי את הסגנונות של האפליקציה, אבל עדיין יש בעיה מטרידה כשאנחנו מעבירים משימות ממקום למקום:
כשאנחנו מתחילים לגרור את המשימה "Buy milk" , אנחנו רואים שני כרטיסים לאותה משימה – אחד שאנחנו גוררים וזו שבנתיב השחייה. CDK Angular מספקת לנו שמות מחלקות CSS שיכולים לעזור לנו לפתור את הבעיה.
צריך להוסיף את שינויי העיצוב הבאים לתחתית של src/app/app.component.css
:
src/app/app.component.css
.cdk-drag-animating {
transition: transform 250ms;
}
.cdk-drag-placeholder {
opacity: 0;
}
בזמן שאנחנו גוררים רכיב, CDK אנגלי כדי לוודא שהרכיב הזה לא יוצג, אנחנו מגדירים את מאפיין השקיפות בכיתה cdk-drag-placeholder
. ה-CDK עומד להוסיף את ה-placeholder.
בנוסף, כאשר משחררים רכיב, CDK מוסיף את המחלקה cdk-drag-animating
. כדי להציג אנימציה חלקה במקום הצמדה ישירה של הרכיב, אנחנו מגדירים מעבר עם משך הזמן 250ms
.
אנחנו רוצים גם לבצע התאמות קלות בסגנונות המשימה. ב-task.component.css
יש להגדיר את הרכיב המארח ל-block
ולהגדיר שוליים:
src/app/task/task.component.css
:host {
display: block;
}
.item {
margin-bottom: 10px;
cursor: pointer;
}
8. עריכה ומחיקה של משימות קיימות
כדי לערוך ולהסיר משימות קיימות, אנחנו נעשה שימוש חוזר ברוב הפונקציונליות שכבר הוטמעו! כשהמשתמש לוחץ לחיצה כפולה על משימה, אנחנו פותחים את ה-TaskDialogComponent
ומאכלסים את שני השדות בטופס ב-title
וב-description
של המשימה.
אנחנו נוסיף ללחצן TaskDialogComponent
לחצן מחיקה. כאשר המשתמש ילחץ עליה, אנחנו נעביר הוראה למחיקה שמסתיימת בAppComponent
.
השינוי היחיד שאנחנו צריכים לבצע ב-TaskDialogComponent
הוא בתבנית שלה:
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>
הלחצן הזה מציג את סמל המחיקה של החומר. כשהמשתמש לוחץ עליו, אנחנו סוגרים את תיבת הדו-שיח ומעבירים את המילה ליטרל של האובייקט { task: data.task, delete: true }
כתוצאה מכך. כמו כן, אנחנו מגדירים את הלחצן כמעוגל באמצעות mat-fab
, מגדירים את הצבע שלו כראשי ומציגים אותו רק כשנתוני תיבת הדו-שיח מופעלים למחיקה.
שאר ההטמעה של תכונות העריכה והמחיקה מופיעה בדף AppComponent
. יש להחליף את השיטה editTask
שלה בשיטות הבאות:
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
:
- רשימה של סוג
'done' | 'todo' | 'inProgress',
שהוא סוג איחוד מילולי עם ערכים שתואמים למאפיינים המשויכים למאגרי השחייה הנפרדים. - המשימה הנוכחית שאנחנו רוצים לערוך.
בגוף השיטה, אנחנו פותחים תחילה מופע של TaskDialogComponent
. אנחנו מגדירים ב-data
ליטרל של אובייקט, המציין את המשימה שאנחנו רוצים לערוך. בנוסף, הוא מאפשר להפעיל את לחצן העריכה בטופס על ידי הגדרת המאפיין enableDelete
כ-true
.
כשאנחנו מקבלים את התוצאה מתיבת הדו-שיח, אנחנו מטפלים בשני תרחישים:
- אם הדגל
delete
מקבל את הערךtrue
(כלומר, כשמשתמש לוחץ על לחצן המחיקה), המשימה תוסר מהרשימה המתאימה. - לחלופין, אנחנו מחליפים את המשימה באינדקס הנתון במשימה שקיבלנו מתוצאת תיבת הדו-שיח.
9. יצירה של פרויקט Firebase חדש
הגיע הזמן ליצור פרויקט Firebase חדש!
- נכנסים למסוף Firebase.
- יוצרים פרויקט חדש בשם "KansanFire".
10. הוספה של Firebase לפרויקט
בקטע הזה נשלב את הפרויקט שלנו עם Firebase! צוות Firebase מציע את החבילה @angular/fire
, הכוללת שילוב בין שתי הטכנולוגיות. כדי להוסיף תמיכה ב-Firebase לאפליקציה, צריך לפתוח את ספריית הבסיס של סביבת העבודה ולהפעיל את:
ng add @angular/fire
פקודה זו מתקינה את החבילה של @angular/fire
וישאלת כמה שאלות. במסוף, אתם אמורים לראות משהו כמו:
בינתיים, ההתקנה תפתח חלון דפדפן שיאפשר לך לאמת את החשבון שלך ב-Firebase. לבסוף, הוא מבקש לבחור פרויקט Firebase ויוצר קבצים בדיסק.
בשלב הבא, עלינו ליצור מסד נתונים של Firestore! בקטע "Cloud Firestore" לוחצים על & "יצירת מסד נתונים& "
לאחר מכן, יוצרים מסד נתונים במצב בדיקה:
לבסוף, בוחרים אזור:
הדבר היחיד שנותר עכשיו הוא להוסיף את תצורת Firebase לסביבה שלך. הגדרות הפרויקט מופיעות במסוף Firebase.
- לוחצים על סמל גלגל השיניים לצד 'סקירה כללית של הפרויקט'.
- בוחרים הגדרות פרויקט.
בקטע "האפליקציות", בוחרים ב&&אפליקציית אינטרנט;&
לאחר מכן יש לרשום את הבקשה וחשוב להפעיל את "Firebase Hosting" :
אחרי שלוחצים על "רישום אפליקציה" אפשר להעתיק את ההגדרה אל src/environments/environment.ts
:
בסופו, קובץ התצורה אמור להיראות כך:
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. העברת הנתונים ל-Firestore
עכשיו, לאחר שהגדרנו את ה-SDK של Firebase, נשתמש ב-@angular/fire
כדי להעביר את הנתונים שלנו ל-Firestore! ראשית, נייבא את המודולים הדרושים לנו ב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 {}
מאחר שאנחנו נשתמש ב-Firestore, עלינו להחדיר את AngularFirestore
בבנא של AppComponent
&33:
src/app/app.component.ts
...
import { AngularFirestore } from '@angular/fire/firestore';
@Component({...})
export class AppComponent {
...
constructor(private dialog: MatDialog, private store: AngularFirestore) {}
...
}
בשלב הבא, אנחנו מעדכנים את האופן שבו אנחנו מפעילים את מערכי הנתיבים:
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[]>;
...
}
כאן אנחנו משתמשים ב-AngularFirestore
כדי לקבל את התוכן של האוסף ישירות ממסד הנתונים. חשוב לשים לב שהפונקציה valueChanges
מחזירה תצפית במקום מערך, ואנחנו גם מציינים ששדה המזהה עבור המסמכים באוסף הזה צריך להיות id
כדי להתאים לשם שבו אנחנו משתמשים בממשק Task
. ניתן להציג על ידי valueChanges
אוסף של משימות בכל פעם שהוא משתנה.
אנחנו עובדים עם מקומות גלויים במקום מערכים, ולכן עלינו לעדכן את האופן שבו אנחנו מוסיפים, מסירים ועורכים משימות, ואת הפונקציונליות של העברת משימות בין בריכות שחייה. במקום לשנות את מערכי הזיכרון שלנו, נשתמש ב-Firebase SDK כדי לעדכן את הנתונים במסד הנתונים.
קודם כול, נראה איך ייראו הסידור מחדש. יש להחליף את השיטה drop
בשדה src/app/app.component.ts
ב:
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
);
}
בקטע הקוד שלמעלה מודגש הקוד החדש. כדי להעביר משימה מנתיב השחייה הנוכחי ליעד, אנחנו נסיר את המשימה מהאוסף הראשון ונוסיף אותה למשימה השנייה. אנחנו מבצעים שתי פעולות שאנחנו רוצים להיראות כמו אחת (כלומר, מבצעות את הפעולה האטומית), ולכן אנחנו מפעילים אותן בעסקה של Firestore.
בשלב הבא, נעדכן את השיטה של editTask
כדי להשתמש ב-Firestore! בתוך ה-handler של תיבת הדו-שיח הסגירה, עלינו לשנות את שורות הקוד הבאות:
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);
}
});
...
אנחנו ניגשים למסמך היעד המשויך למשימה שאנחנו מבצעים באמצעות ה-SDK של Firestore, ומוחקים או מעדכנים אותו.
לבסוף, עלינו לעדכן את השיטה ליצירת משימות חדשות. החלפת this.todo.push('task')
ב: this.store.collection('todo').add(result.task)
.
שימו לב שעכשיו האוספים שלנו אינם מערכים, אך ניתן לתעד. כדי שניתן יהיה להציג אותם באופן חזותי, אנחנו צריכים לעדכן את התבנית של AppComponent
. יש להחליף את כל הגישה בנכסים של todo
, inProgress
וגם done
ב-todo | async
, ב-inProgress | async
וב-done | async
בהתאמה.
הצינור האסינכרוני נרשם באופן אוטומטי למודעות הניתנות לצפייה המשויכות לאוספים. כשאפשר לצפות בערך חדש, Angular מריץ באופן אוטומטי זיהוי שינויים ומעבד את המערך שנפלט.
לדוגמה, בואו נבחן את השינויים שאנחנו צריכים לבצע בנתיב השחייה של 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>
כשאנחנו מעבירים את הנתונים להוראה cdkDropList
, אנחנו מחילים צינור אסינכרוני. התוצאה זהה בתוך ההוראה *ngIf
, אבל חשוב לזכור שאנחנו משתמשים גם בשרשור אופציונלי (שנקרא גם אופרטור ניווט בטוח ב-Angular), כדי לקבל גישה לנכס length
כדי לא לקבל שגיאת זמן ריצה אם todo | async
לא null
או undefined
.
עכשיו כשיוצרים משימה חדשה בממשק המשתמש ופותחים את Firestore, אמורה להופיע תמונה כזו:
12. שיפור העדכונים האופטימיים
באפליקציה אנחנו מבצעים עדכונים אופטימיים כרגע. יש לנו מקור מידע אמיתי ב-Firestore, אבל בו-זמנית יש לנו עותקים מקומיים של המשימות. כאשר כל אחד מהגלויים המשויכים לאוספים פולט, אנחנו מקבלים מגוון משימות. כשפעולה של משתמש משנה את המדינה, אנחנו מעדכנים קודם את הערכים המקומיים ולאחר מכן מפיצים את השינוי ל-Firestore.
כשאנחנו מעבירים משימה מנתיב אחד לשחייה אחרת, אנחנו מפעילים את transferArrayItem,
. הפונקציה מופעלת במופעים מקומיים של המערכים שמייצגים את המשימות בכל נתיב שחייה. ה-SDK של Firebase מתייחס למערכים האלה ללא שינוי. כלומר, בפעם הבאה שאנג'ל יפעיל את השינויים, נקבל מופעים חדשים שלהם, מה שיגרום למצב הקודם לפני שנעביר את המשימה.
במקביל, אנחנו מפעילים עדכון ל-Firestore וה-SDK של Firebase מפעיל עדכון עם הערכים הנכונים, כך שבעוד כמה אלפיות שנייה ממשק המשתמש יעבור למצב הנכון. כך, המשימה שהעברנו עכשיו עוברת מהרשימה הראשונה לרשימה הבאה. אפשר לראות זאת טוב בקובץ ה-GIF שבהמשך:
הדרך הנכונה לפתור את הבעיה הזו משתנה בהתאם לבקשה, אך בכל המקרים עלינו לוודא שאנו שומרים על מצב עקבי עד שהנתונים שלנו יתעדכנו.
אנחנו יכולים לנצל את היתרונות של BehaviorSubject
, שמכסה את הצופה המקורי שאנחנו מקבלים מ-valueChanges
. מאחורי הקלעים, מערכת BehaviorSubject
שומרת מערך משותף של פריטים ששומרים את העדכון מ-transferArrayItem
.
כדי ליישם תיקון, כל מה שאנחנו צריכים לעשות הוא לעדכן את 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[]>;
...
}
כל מה שאנחנו עושים בקטע הקוד שלמעלה הוא ליצור BehaviorSubject
, שמפיק ערך בכל פעם שגלויים המשויכים לאוסף.
הכול פועל כמצופה, כי ב-BehaviorSubject
נעשה שימוש חוזר במערך ההפעלה בכל זיהוי שינויים, והוא מתעדכן רק כשמקבלים ערך חדש מ-Firestore.
13. פריסת האפליקציה
כל מה שאנחנו צריכים לעשות כדי לפרוס את האפליקציה שלנו:
ng deploy
הפקודה הזו:
- יש לבנות את האפליקציה עם תצורת הייצור שלה, וליישם אופטימיזציות בזמן הידור.
- פרסו את האפליקציה שלכם ב-Firebase Hosting.
- יוצרים פלט של כתובת URL כדי להציג תצוגה מקדימה של התוצאה.
14. מזל טוב
מזל טוב, בנית בהצלחה לוח קנקן עם Angular ו-Firebase!
יצרתם ממשק משתמש עם שלוש עמודות שמייצגות את הסטטוס של משימות שונות. באמצעות CDK האנגלי, הטמעתם גרירה ושחרור של משימות בכל העמודות. לאחר מכן, באמצעות חומר זוויתי, יצרתם טופס ליצירת משימות חדשות ועריכת משימות קיימות. בשלב הבא, למדת איך להשתמש ב-@angular/fire
והעברת את כל מצב האפליקציה ל-Firestore. לבסוף, פרסתם את האפליקציה לאירוח ב-Firebase.
מה השלב הבא?
זכור כי פרסנו את האפליקציה באמצעות תצורות בדיקה. לפני פריסת האפליקציה שלכם בסביבת הייצור, ודאו שהגדרתם את ההרשאות הנכונות. כאן מוסבר איך לעשות זאת.
בשלב זה, אנחנו לא שומרים את הסדר של המשימות הנפרדות בנתיב שחייה מסוים. כדי ליישם זאת, אפשר להשתמש בשדה הזמנה במסמך המשימה ולמיין אותו.
בנוסף, יצרנו את לוח קנקן עבור משתמש אחד בלבד, כלומר, יש לנו לוח קנקן יחיד לכל מי שפותחים את האפליקציה. כדי להטמיע לוחות נפרדים למשתמשים שונים באפליקציה, יש לשנות את מבנה מסד הנתונים. כאן אפשר לקרוא על השיטות המומלצות של Firestore.