iX 7/2017
S. 122
Praxis
Web Application Framework
Aufmacherbild

Webprogrammierung mit Angular, Teil 3

Feinschliff

Im letzten Teil des Angular-Tutorials geht es um Menüs, Dialogfenster, das Aufhübschen der Aufgabenverwaltung per Animation und den Anmeldedialog.

Aufbauend auf der anfangs angelegten Struktur des Projekts „MiracleList“ und der Anzeige von Daten von REST-Diensten kamen im zweiten Teil Elemente zum Bearbeiten von Kategorien, Aufgaben und Teilaufgaben hinzu, unter Mithilfe von Angular-Routing. Nun soll die Anwendung schrittweise zum einsatzfertigen Produkt verfeinert, getestet und endgültig kompiliert werden.

Ein Kontext- und ein „Hamburger“-Menü erleichtern die Arbeit mit MiracleList (Abb. 1).

Wie beim Wunderlist-Original soll das Löschen von Aufgaben per Kontextmenü erfolgen. Da Angular so etwas von Haus aus nicht anbietet, kommt eine Open-Source-Umsetzung bei GitHub zum Einsatz (siehe „Alle Links“, [a]). Aus dem Projekt braucht man die Datei angular2-contextmenu.ts und den ganzen Ordner src. Beide zusammen bilden ein eigenständiges Angular-Modul, das in /Util/angular2-contextmenu/ liegen soll. Anschließend ist es in app/app. module.ts zu registrieren:

import { ContextMenuModule } from
  '../Util/angular2-contextmenu/ ⤦
 angular2-contextmenu';
...
 [   BrowserModule, … , ContextMenuModule  ], …

Listing 1: Template für AppComponent (Ausschnitt)

<context-menu #taskMenu>
  <template contextMenuItem let-item
        (execute)="editTask($event.item)">
    <span class="glyphicon glyphicon-pencil"
         aria-hidden="true"></span>
    <b>Bearbeite Aufgabe:</b> {{item.title}}
  </template>
  <template contextMenuItem let-item
        (execute)="deleteTask($event.item)">
    <span class="glyphicon glyphicon-remove"
        aria-hidden="true"></span>
    <b>Lösche Aufgabe:</b> {{item.title}}
  </template>
</context-menu>

Listing 2: Kontextmenü und Sortierung für Aufgaben

<ol class="list-group" dnd-sortable-container
    [sortableData]="taskSet">
        <!-- ---------- eine Aufgabe im Block ausgeben
             mit Kontextmenü und Sortierbarkeit -->
    <li *ngFor="let t of taskSet; let i = index"
           dnd-sortable [sortableIndex]="i" (drop)="reorder(t,  i)"
           (click)="showTaskDetail(t)" title="Aufgabe #{{t.taskID }}..."
            class="list-group-item" [style.background-color]
                     ="t.taskID == task?.taskID ? '#E0EEFA' : 'white'"
            [contextMenu]="taskMenu" [contextMenuSubject]="t">
         <input type="checkbox" [(ngModel)]="t.done"
            (change)="changeDone(t)"   name="done{{t.taskID}}" id="done{{t.taskID}}"
            class="form-control MLcheckbox">
         <b>{{ t.title }} </b>
         <span class="badge badge-important"
               title="Wichtigkeit: {{t.importance |importance}}">
                  {{t.importance | importance}}</span>
         <div>{{getUndoneSubTaskSet(t).length}} offene Teilaufgaben</div>
         <div *ngIf="t.due"
            [ngClass]="{'text-danger': t.due<today,'text-warning': t.due==today,
            'text-success':t.due>today}">fällig {{t.due | amTimeAgo}}</div>
     </li>
</ol>

In app.component.html kann man danach mit den Elementen context-menu und template ein Kontextmenü definieren (siehe Listing 1). Nach # folgt sein Name (#taskMenu). [contextMenu]=“taskMenu“ bindet es an beliebiger Stelle ein, und per [contextMenuSubject]=“t“ bekommt es das aktuelle Task-Objekt übergeben (siehe Listing 2). Das Menü kann es dadurch zum Anzeigen und als Parameter für den Callback execute nutzen (siehe Listing 1).

Eine gelöschte Aufgabe darf nicht mehr in der dritten Spalte erscheinen. Daher wechselt AppComponent auf die Hauptansicht mit breiter Aufgabenliste zurück.

Nachfragen beim Löschen

Etwas ohne Nachfrage zu löschen, kann zu Bedienfehlern und zu Unzufriedenheit führen. Deshalb soll ein vorgeschalteter Dialog um Bestätigung des Löschens bitten. Die Unterstützung für Dialoge („Pop-up-Fenster“) fehlt leider ebenfalls im Standardumfang von Angular. Hier kommt das Modul angular2-modal [b] zum Einsatz, das npm install angular2-modal --save installiert. In app.module.ts muss der Entwickler eintragen:

import { ModalModule } from 'angular2-modal';
import { BootstrapModalModule }
 from 'angular2-modal/plugins/bootstrap';
 [  BrowserModule, …, ModalModule.forRoot(),  BootstrapModalModule  ], })

Eine Komponente, die ein Dialogfenster öffnen soll, muss drei Klassen importieren und im Konstruktor per Dependency Injection empfangen. Außerdem ist das ViewContainerRef-Objekt von Angular dem defaultViewContainer des Overlay-Objekts von angular2-modal zuzuweisen:

import { ViewContainerRef } ⤦
  from '@angular/core';
import { Overlay } from 'angular2-modal';
import { Modal } from
    'angular2-modal/plugins/bootstrap';
...
constructor(private overlay: Overlay,
    private vcr: ViewContainerRef, public
    modal: Modal) {
  overlay.defaultViewContainer = vcr;
 }

Listing 3: Komponente AppComponent (Ausschnitt)

deleteTask(t: Task) {
  // Dialog anzeigen
  var dialog = this.modal.confirm()
   .okBtn('Löschen')
   .cancelBtn('Abbrechen')
   .size('lg')
   .isBlocking(true)
   .showClose(true)
   .keyboard(27)
   .title('Löschen bestätigen')
   .body(`Soll die Aufgabe <b>${t.title}</b> und alle `
    +  "damit verbundenen Details wirklich für
    +  "<b>immer gelöscht</b> werden?")
   .open();

  // Dialog-Ergebnis (Promise) auswerten
  dialog.then((d) => d.result)
   .then((ok) => {
    this.miracleListProxy.deleteTask(
          this.communicationService.token, t.taskID)
        .subscribe(
       x => {
        console.log("Task GELÖSCHT", t.taskID)
        this.task = null;
        this.updateColumn2();
     });
   },
   (cancel) => { // nichts tun }); 
   });
 }

Danach kann der Entwickler in der Komponente mit den Methoden alert(), prompt() und confirm() des Objekts this. modul beliebige Dialogfenster öffnen. Listing 3 zeigt die Methode deleteTask() in AppComponent. Sie fragt nach, ob eine Aufgabe wirklich gelöscht werden soll.

DOM-Elemente animieren

Während viele GUI-Elemente im Kern von Angular noch fehlen, gehört ein Animationsframework bereits dazu. Damit kann man Bewegungen von DOM-Elementen und flüssige Übergänge zwischen den Komponenten erstellen. Basis für Angular-Animationen ist der kommende W3C-Standard „Web Animations“ [c], den jedoch bislang nur Chrome, Firefox, Opera und der Android-Browser umgesetzt haben [d]. Ein Polyfill [e] rüstet die Funktionen für andere Browser nach; man installiert es mit:

npm install --save web-animations-js

Anschließend ist in der Datei /src/Poly fills.ts der Eintrag import web-anima tions-js’ zu ergänzen.

Ziel einer Animation können alle im W3C-Standard vorgesehenen Eigenschaften sein, etwa Position, Größe, Farbe, Ränder und Transformationen. Anzugeben sind Endzustand und Dauer oder mehrere Schritte anhand sogenannter Keyframes.

Listing 4: Bewegungseffekte (Ausschnitt)

import {trigger, state, animate, style, transition} from '@angular/core';

export function slideToLeft() {
  return trigger('routerTransition', [
    state('void', style({position:'fixed', width:'100%'}) ),
    state('*', style({position:'fixed', width:'100%'}) ),
    transition(':enter', [
      style({transform: 'translateX(100%)'}),
      animate('0.5s ease-in-out', style({transform: 'translateX(0%)'}))
    ]),
    transition(':leave', [
      style({transform: 'translateX(0%)'}),
      animate('0.5s ease-in-out', style({transform: 'translateX(-100%)'}))
    ])
  ]);
}

Listing 5: Festlegen einer Animation

import { slideToLeft } from '../Util/RouterAnimations';

@Component({
 selector: 'TaskEdit',
 templateUrl: './TaskEdit.component.html',
  animations: [slideToLeft()],
  host: {'[@routerTransition]': ''}
})

Listing 4 zeigt einen Bewegungseffekt nach links. Er soll das Erscheinen und Verschwinden der TaskEditComponent begleiten, was Zusätze zur @Component-Annotation erreichen (Listing 5).

Nun soll das Sortieren der Aufgaben per Ziehen und Fallenlassen ermöglicht werden. Für diese Funktion gibt es auf GitHub kostenfreien Open-Source-Code: Angular 2 Drag-and-Drop (kurz: ng2-dnd) [f]. Um ihn zu nutzen, bekommt das ol-Element die Erweiterung dnd-sortable-container [sortableData]=“taskSet“, deren zweiter Teil auf die sortierte Menge der Aufgaben verweist (siehe Listing 2). Die Ausgabe der einzelnen Aufgaben im li-Element erhält den Zusatz