npm install -g @angular/cli
angular-todo-list
.ng new todo-list-app --strict
todo-list-app
und öffne diesen in Visual Studio Code.ng serve
Wenn du das Projekt erfolgreich angelegt und gestartet hast, solltest du im Browser folgende Seite sehen:
Styles, die für das gesamte Projekt gelten sollen, findest du unter src/styles.scss
. Dort ändern wir als erstes die Schriftart und den Margin für den body
:
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
}
Das HTML Grundgerüst deiner App wird im File src/index.html
festgelegt. Dort kannst du z.B. externe CSS oder Javascript Libraries wie Bootstrap einbinden. Für unser Beispiel verwenden wir diesmal keine Library und müssen daher im Moment auch nichts ändern im index.html
.
Angular Apps bestehen aus mehrere Components und die erste Component wird beim Anlegen des Projekts auch gleich automatisch angelegt - die *AppComponent. Sie besteht aus den folgenden Dateien:
src/app/app.component.html
- für das HTML der Componentsrc/app/app.component.scss
- für die Styles, die Styles gelten nur für diese Component und können in anderen Components nicht verwendet werdensrc/app/app.component.spec.ts
- damit kann die Logik von Components getestet werden, wir werden in diesem Beispiel noch keine Tests schreibensrc/app/app.component.ts
- in diesem File ist der Typescript Code zur Component enthaltenWenn du dir den generierten Typescript Code ansiehst, siehst du, dass hier die Urls für das Template (HTML) und die Styles (SCSS) angegeben sind. Außerdem wird mit dem Selector festgelegt, wie die Component verwendet werden kann. app-root
heißt, dass die Komponente im HTML mit <app-root></app-root>
verwendet werden kann.
Wenn du jetzt einen Blick in src/index.html
wirfst, dann siehst du, dass genau diese Komponente im <body>
verwendet wird.
Lösche dazu den ganzen generierten Content im src/app/app.component.html
und ersetze ihn durch folgendes HTML:
<nav>
<ul>
<li>Todo Liste</li>
<li>Dashboard</li>
</ul>
</nav>
<main>
<router-outlet></router-outlet>
Test
</main>
Das Tag <router-outlet>
ist ein Platzhalter, in dem dann weitere Komponenten die wir erst erstellen müssen, angezeigt werden können, sobald der User auf einen Menüpunkt klickt.
Für eine hübschere Darstellung fügen wir noch ein paar Styles in src/app/app.component.scss
ein:
ul {
position: fixed;
top: 0;
left: 0;
margin: 0;
padding: 0 40px;
width: 100%;
background-color: #ffeb3b;
height: 40px;
line-height: 40px;
}
li {
display: inline;
margin-right: 40px;
}
main {
margin-top: 40px;
padding: 40px;
}
Deine Seite sollte dann so aussehen:
Wir wollen jetzt für jeden Menüpunkt eine Komponente erstellen. Führe dazu entweder im Command Prompt im Ordner todo-list-app
oder in einem neuen Terminal Fenster im Visual Studio Code folgende Commands aus:
ng g component todo-list
ng g component dashboard
Die beiden Components können jetzt schon mit den generierten Selektoren app-dashboard
und app-todo-list
verwendet werden. Damit sie auch über eine eigene Url erreichbar sind, müssen wir im src/app/app-routing.module.ts
neue Routes dafür einfügen:
const routes: Routes = [
{ path: 'dashboard', component: DashboardComponent },
{ path: 'todo-list', component: TodoListComponent }
];
Über die Urls http://127.0.0.1:4200/todo-list
und http://127.0.0.1:4200/dashboard
können die beiden Komponenten geladen werden.
Im src/app/app.component.html
können wir die Testausgabe löschen. Stattdessen fügen wir in der Navigationsleiste Links zu den beiden neuen Komponenten hinzu.
<nav>
<ul>
<li><a routerLink="/todo-list">Todo Liste</a></li>
<li><a routerLink="/dashboard">Dashboard</a></li>
</ul>
</nav>
<main>
<router-outlet></router-outlet>
</main>
Das Ergebnis sollte jetzt so aussehen:
Als erstes legen wir ein Interface für die Items in unsere Todo Liste an:
ng g interface shared/todo
Im Interface legen wir alle Properties fest, die ein Todo haben muss:
export interface Todo {
id: number;
description: string;
dueDate: Date;
doneDate?: Date;
}
Dieses Interface können wir jetzt in src/app/todo-list-component.ts
verwenden. Wir legen dort ein public Property mit einigen Todo List Items an:
public todos: Todo[] = [
{ id: 1, description: 'Mathe Ferien-Hausübung', dueDate: new Date(2020, 9, 21) },
{ id: 2, description: 'Geburtstagsgeschenk Oma', dueDate: new Date(2020, 8, 20) },
{ id: 3, description: 'Zimmer aufräumen', dueDate: new Date(2020, 8, 14) }
];
Diese Items können wir jetzt im HTML anzeigen. Im einfachsten Fall können wir die Variable todos mit {{ todos }}
ausgeben. Das würde [object Object],[object Object],[object Object]
ausgeben. Wir können auch komplexere Ausdrücke wie z.B. {{ todos.length }}
verwenden, um die Anzahl der Items auszugeben.
Wenn wir die Items filter möchten, so dass z.B. nur noch Items ohne doneDate
ausgegeben werden, dann können wir das über eine Methode in src/app/todo-list-component.ts
lösen. Hier können wir auch gleich noch nach dem Fälligkeitsdatum sortieren:
getOpenTodos(): Todo[] {
return this.todos.filter(t => !t.doneDate).sort((a, b) => a.dueDate < b.dueDate ? -1 : 1);
}
Im HTML können wir die Methode jetzt folgendemaßen verwenden:
<h2>Todos</h2>
<div *ngFor="let item of getOpenTodos()">
{{ item.dueDate | date:'EE, dd.MM.' }} {{ item.description }}
</div>
<p>Items: {{ getOpenTodos().length }}</p>
Damit bekommen wir folgendes Ergebnis:
Wir könnten jetzt die Methoden zum Hinzufügen und Ändern von Items auch direkt in todo-list.component.ts
machen. Um die Todo Liste später aber an mehreren Stellen verwenden zu können (z.B. im Dashboard), legen wir dazu ein neues Service an:
ng g service shared/todo-list
In diesem Service bieten wir Methoden zum Lesen und Ändern der Todo Liste an. Man kann in diesem Service dann auch den Zugriff auf ein Webservice einbauen, um die Daten aus einer Datenbank zu lesen.
Der Code für das Service könnte z.B. folgendemaßen aussehen:
import { Injectable } from '@angular/core';
import { Todo } from './todo';
@Injectable({
providedIn: 'root'
})
export class TodoListService {
public todos: Todo[] = [];
constructor() { }
public getTodos(done?: boolean): Todo[] {
return this.todos
.filter(t => done === undefined || (done && t.doneDate) || (!done && !t.doneDate))
.sort((a, b) => a.dueDate.getTime() - b.dueDate.getTime());
}
public addTodo(description: string, dueDate: Date): void {
let newId = 0;
if (this.todos.length) {
newId = Math.max(...this.todos.map(t => t.id)) + 1;
}
this.todos.push({ id: newId, description: description, dueDate: dueDate });
}
public deleteTodoById(id: number): void {
const index = this.todos.findIndex(t => t.id === id);
if (index >= 0) {
this.todos.splice(index, 1);
}
}
public updateTodoById(id: number, description: string, dueDate: Date): void {
const index = this.todos.findIndex(t => t.id === id);
if (index >= 0) {
this.todos[index].description = description;
this.todos[index].dueDate = dueDate;
}
}
public toggleDoneStateById(id: number): void {
const index = this.todos.findIndex(t => t.id === id);
if (index >= 0) {
if (this.todos[index].doneDate) {
this.todos[index].doneDate = undefined;
} else {
this.todos[index].doneDate = new Date();
}
}
}
}
In der Komponente können wir jetzt das Property todos
und die Methode getOpenTodos
entfernen. Stattdessen verwenden wir Dependency Injection, um Zugriff auf das Todo List Service zu bekommen. Dazu fügen wir es in den Constructor von todo-list.component.ts
ein:
constructor(public todoListService: TodoListService) { }
Das HTML müssen wir jetzt so ändern, dass das Service anstelle des Properties verwendet wird:
<h2>Todos</h2>
<div *ngFor="let item of todoListService.getTodos(false)">
{{ item.dueDate | date:'EE, dd.MM.' }} {{ item.description }}
</div>
<p>Items: {{ todoListService.getTodos(false).length }}</p>
Damit wir Form Controls mit Bindings verwenden können, müssen wir im app.module.ts
das FormsModule
hinzufügen:
...
import { FormsModule } from '@angular/forms';
@NgModule({
declarations: [
...
],
imports: [
...
FormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
In der todo-list.component.ts
fügen wir jetzt Properties für die description
und das dueDate
ein. Diese verwenden wir im HTML dann zum Data Binding. In der neuen Methode addTodo
fügen wir das Item im Service dann ein.
import { Component, OnInit } from '@angular/core';
import { TodoListService } from '../shared/todo-list.service';
import { formatDate } from '@angular/common';
@Component({
selector: 'app-todo-list',
templateUrl: './todo-list.component.html',
styleUrls: ['./todo-list.component.scss']
})
export class TodoListComponent implements OnInit {
public todoDescription = '';
public todoDueDate = formatDate(new Date(), 'yyyy-MM-dd', 'en');
public showDone = false;
constructor(public todoListService: TodoListService) { }
ngOnInit(): void {
}
public addTodo(): void {
if (this.todoDescription && this.todoDueDate) {
this.todoListService.addTodo(this.todoDescription, new Date(this.todoDueDate));
this.todoDescription = '';
this.todoDueDate = formatDate(new Date(), 'yyyy-MM-dd', 'en');
}
}
}
Jetzt müssen wir noch im HTML eine Möglichkeit bieten, neue Todos zu erfassen und zu löschen:
<h2>Todos</h2>
<div class="addItem">
Neues Todo: <input type="text" [(ngModel)]="todoDescription"> <input type="date" [(ngModel)]="todoDueDate"> <button (click)="addTodo()">Hinzufügen</button>
</div>
<div *ngFor="let item of todoListService.getTodos(false)">
<button (click)="todoListService.toggleDoneStateById(item.id)">Done</button> <button (click)="todoListService.deleteTodoById(item.id)">Löschen</button> {{ item.dueDate | date:'EE, dd.MM.' }} {{ item.description }}
</div>
<p>Todo: {{ todoListService.getTodos(false).length }}<br/>Done: {{ todoListService.getTodos(true).length }}</p>
Wir möchten jetzt noch ein Häkchen hinzufügen, mit dem wir bereits erledigte Items anzeigen können. Dazu fügen wir zuerst in todo-list.component.ts
ein Property showDone
ein:
public showDone = false;
Im HTML können wir jetzt das Häckchen hinzufügen und im *ngFor
dann den richtigen Parameter übergeben. Außerdem werden bereits erledigte Elemente durchgestrichen angezeigt:
<h2>Todos</h2>
<div class="addItem">
Neues Todo: <input type="text" [(ngModel)]="todoDescription"> <input type="date" [(ngModel)]="todoDueDate"> <button (click)="addTodo()">Hinzufügen</button>
</div>
<p>Done anzeigen: <input type="checkbox" [(ngModel)]="showDone"></p>
<div *ngFor="let item of todoListService.getTodos(showDone ? undefined : false)">
<button (click)="todoListService.toggleDoneStateById(item.id)">Done</button>
<button (click)="todoListService.deleteTodoById(item.id)">Löschen</button>
<span [ngStyle]="{'text-decoration': (item.doneDate ? 'line-through' : 'none')}">{{ item.dueDate | date:'EE, dd.MM.' }} {{ item.description }}</span>
</div>
<p>Todo: {{ todoListService.getTodos(false).length }}<br/>Done: {{ todoListService.getTodos(true).length }}</p>
Im Dashboard wollen wir jetzt zwei Balken anzeigen für die noch offenen und die bereits erledigten Todos. In dashboard.component.ts
fügen wir dazu zwei Methoden ein, die uns die jeweiligen Prozentsätze ausrechnen:
import { Component, OnInit } from '@angular/core';
import { TodoListService } from '../shared/todo-list.service';
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.scss']
})
export class DashboardComponent implements OnInit {
constructor(public todoListService: TodoListService) { }
ngOnInit(): void {
}
getTodoPercentage(): number {
const allTodos = this.todoListService.getTodos();
if (allTodos.length) {
return this.todoListService.getTodos(false).length / allTodos.length * 100;
} else {
return 0;
}
}
getDonePercentage(): number {
const allTodos = this.todoListService.getTodos();
if (allTodos.length) {
return this.todoListService.getTodos(true).length / allTodos.length * 100;
} else {
return 0;
}
}
}
Dann fügen wir im HTML zwei divs ein, deren Breite über Data Binding im [ngStyle]
gesteuert wird. Hier verwenden wir die berrechneten Prozentsätze:
<h2>Dashboard</h2>
<div class="bar-chart">
<div [ngStyle]="{ 'width': getTodoPercentage().toString() + '%' }" class="todo-bar">Todo: {{ getTodoPercentage() | number:'1.0-0' }} %</div>
<div [ngStyle]="{ 'width': getDonePercentage().toString() + '%' }" class="done-bar">Done: {{ getDonePercentage() | number:'1.0-0' }} %</div>
</div>
Im Stylesheet müssen wir noch die Höhe und Farben der Balken festlegen:
.bar-chart {
>div {
height: 40px;
margin-bottom: 10px;
white-space: nowrap;
}
.todo-bar {
background-color: #03a9f4;
height: 40px;
}
.done-bar {
background-color: #4caf50;
height: 40px;
}
}