iX 6/2017
S. 128
Praxis
Web Application Framework
Aufmacherbild

Webprogrammierung mit Angular, Teil 2

Formulargedöns

Der zweite Teil des Angular-Tutorials zeigt das Senden von Daten an REST-Dienste, das Erstellen untergeordneter Angular-Komponenten, das Routing zwischen Komponenten und den Bau von Eingabemasken.

Im ersten Teil des Tutorials wurde die Struktur des Projekts „MiracleList“ (siehe „Alle Links“, [a]) angelegt und ein Proxy für die REST-Dienste des vorhandenen Backends [b] erstellt. Außerdem entstanden die Anzeige der Kategorien samt Aufgabenliste und eine Detailansicht zu den Aufgaben. Der Benutzer kann jedoch bislang in der Webanwendung keine Daten ändern.

Den Status von Aufgaben ändern

Die Beispielanwendung MiracleList erlaubt nun das Ändern von Aufgaben (Abb. 1).

Jede Aufgabe in der mittleren Liste (siehe Abbildung 1) besitzt ein großes Kontrollkästchen (die CSS-Klasse WLcheckbox ist in app.component.css deklariert) zum Ändern des Erledigungsstatus:

<input type="checkbox" [(ngModel)]="t.done"
  name="done{{t.taskID}}" id="done{{t.taskID}}"
  (change)="changeDone(t)"
  class="form-control WLcheckbox">

Listing 1: Ausschnitt aus der Komponentenklasse AppComponent

enum DisplayMode { TaskSet = 0, DueTaskSet = 1, Search = 2 };

@Component({
 selector: 'app-root',
 templateUrl: './app.component.html',
 styleUrls: ['./app.component.css']
})
export class AppComponent {

private newCategoryName: string; // neue Kategorie
private newTaskTitle: string; // neue Aufgabe
private searchText: string; // Suchtext
...
private categorySetWithTaskSet: Array<Category>; // Suchergebnisse
private displayMode: DisplayMode;
...

constructor(private miracleListProxy: MiracleListProxy,
  private communicationService: CommunicationService) {
  ...
  // Ereignisbehandlung für taskChangedEvent in TaskEdit
  communicationService.TaskListUpdateEvent.subscribe(
   x => {
    this.updateColumn2();
   }
  );
 }

updateColumn2()
  {
   // unterscheiden, was aktualisiert werden muss    
  switch (this.displayMode) {
     case DisplayMode.TaskSet: this.showTaskSet(this.category.categoryID); break; 
     case DisplayMode.Search: this.search(true);  break;
     case DisplayMode.DueTaskSet: this.getDueTaskSet(true);  break;
    }
  }

search(showTask: boolean = false)
 {
  if (!this.searchText) return "";
  this.miracleListProxy.search(this.communicationService.token,
    this.searchText).subscribe(x=>
    { console.log("suche ERGEBNIS", this.searchText, x);
      if (!showTask) this.task = null;
      this.taskSet = null;
      this.category = null;
      this.categorySetWithTaskSet = x;
  });
 }

changeDone(t: Task) {
  console.log("Task ÄNDERN", t);
  this.miracleListProxy.changeTask(this.communicationService.token,    
       t).subscribe(
   x => { console.log("Task GEÄNDERT", x)
          this.updateColumn2();
   });
 }

createTask() {
  var t = new Task();
  t.taskID = 0; // notwendig für Server, da der die ID vergibt
  t.title = this.newTaskTitle;
  t.categoryID = this.category.categoryID;
  t.importance = Importance.B;
  t.created = new Date();
...
  t.done = false;
  this.miracleListProxy.createTask(this.communicationService.token,    
       t).subscribe(
   x => {
    console.log("Task ERZEUGT", x)
    this.showTaskSet(this.category);
    this.newTaskTitle = "";
   });
 }

}

Wichtig in dieser Zeile sind die Zwei-Wege-Datenbindung von [(ngModel)] an die Eigenschaft done des Task-Objekts und der eindeutige Name für jedes Kontrollkästchen mit name="done{{t.taskID}}". Die Schreibweise [(ngModel)] heißt auch „Banana in a Box“. Listing 1 zeigt die mit dem Kontrollkästchen verbundene Methode changeDone() in der Klasse AppComponent. Sie ruft die REST-Operation /ChangeTask auf und übergibt ihr das geänderte Task-Objekt.

Neue Aufgaben erstellen

Ähnlich sieht es bei Teilaufgaben aus. Hier gewährleistet zudem das bedingte Anwenden eines Style-Attributes, dass – wie im Wunderlist-Original – eine erledigte Unteraufgabe durchgestrichen wird:

<span [style.text-decoration]="st.done
  ? 'line-through': 'none'">
  {{st.title }}</span>

Als Nächstes soll ein Eingabefeld für neue Aufgaben oben in Spalte 2 erscheinen:

<input type="text" class="form-control"
  name="newTaskTitle" [(ngModel)]="newTaskTitle"
  (change)="createTask()"
  placeholder="neue Aufgabe...">

Dazu korrespondieren in der Komponentenklasse (Listing 1) die Eigenschaft newTaskTitle und die Methode createTask(). Letztere erzeugt ein Task-Objekt mit dem neuen Titel und einigen Standardvorgaben. Die von der Datenbank auf dem Server vergebene taskID muss der Entwickler mit 0 initialisieren. Sonst besäße sie den Wert undefined und der Browser serialisierte sie in JSON als null. Das aber mag das Backend nicht, in dem das Attribut Primärschlüssel ist und daher nicht null sein darf. Nach dem Aufruf der REST-Operation /CreateTask lädt der Webclient die Liste der Aufgaben mit this.showTaskSet(this.category) erneut.

Ähnlich verläuft das Hinzufügen einer Teilaufgabe zu einer Aufgabe, also per Eingabefeld für die Eigenschaft newSubTaskTitle der Komponentenklasse:

<input type="text" class="form-control"
  name="newSubTaskTitle"
  [(ngModel)]="newSubTaskTitle"
  (change)="createSubTask()"
  placeholder="neue Teilaufgabe...">

Die Methode createSubTask() muss dann ein SubTask-Objekt initialisieren, bevor sie /ChangeTask auf dem Server aufruft.

Listing 2: Datei /SubTaskList/SubTaskList.component.ts

import { Component} from '@angular/core';
import { Input, Output, EventEmitter } from '@angular/core';
import { Task, SubTask } from '../app/MiracleListProxy';
import { MiracleListProxy } from '../app/MiracleListProxy';

@Component({
 selector: 'SubTaskList',
 templateUrl: './SubTaskList.component.html'
})

export class SubTaskListComponent  {
 constructor(private miracleListProxy: MiracleListProxy) {}

 @Input()
 public task: Task;
 @Output()
 subTaskListChangedEvent = new EventEmitter();

 private newSubTaskTitle: string;

 createSubTask() {
  var st = new SubTask();
  st.subTaskID = 0;
  st.title = this.newSubTaskTitle;
  st.created = new Date();
  ...
  this.task.subTaskSet.push(st);
  this.changeTask();
 }

 changeTask() {
  this.miracleListProxy.changeTask(this.task).subscribe(
   x => {
    console.log("Task GEÄNDERT", x)
    this.task = x;
    this.newSubTaskTitle = "";
    this.subTaskListChangedEvent.emit(this.task);
   });
 }

 removeSubTask(st: SubTask) {
  console.log("Subtask LÖSCHEN", st)
  var index = this.task.subTaskSet.indexOf(st);
  this.task.subTaskSet.splice(index, 1);
  this.miracleListProxy.changeTask(this.task).subscribe(
   x => {
    console.log("Subtask GELÖSCHT", st)
    this.subTaskListChangedEvent.emit(this.task);
   });
 }
}

Bei der Initialisierung ist neben der 0 für den Primärschlüssel die doppelte Beziehung zwischen Task- und SubTask-Objekt zu beachten, die this.task.subTaskSet.push(st) und st.taskID = this.task.taskID herstellen (siehe Listing 2). Der Aufruf von push() gewährleistet, dass Angular neue Teilaufgaben zum Server sendet. Die Initialisierung von st.taskID ist notwendig, da der Server auch für Subtasks keinen null-Wert erhalten mag. .NET-Entwickler sind vom Entity Framework gewohnt, dass eine Beziehung über eine Navigationseigenschaft automatisch den Fremdschlüssel per Relationship Fixup verändert und umgekehrt. Eine solche Funktion gibt es im Browser aber nicht ohne eigene Programmierung. Initialisiert man st.taskID mit 0, erledigt der mit dem Entity Framework erstellte Server das Relationship Fixup.

Neben der im ersten Teil des Tutorials erstellten einfachen Aufgabenliste bietet MiracleList eine weitere Liste an (siehe Abbildung 1), deren zweite Spalte wiederum eine Liste enthält, und zwar mit Kategorien und ausgewählten Aufgaben. Sie erscheint, wenn der Benutzer per Eingabefeld sucht oder auf „Fällige Aufgaben“ klickt. Für die Suche nach Aufgaben erhält die erste Spalte unten ein Eingabefeld: