Single Page Applications mit AngularJS, Teil 1: Erste Schritte

Werkzeuge  –  4 Kommentare

Datenbindung und das Einarbeiten externer Dienste sind nur zwei der Aufgaben, um die sich Entwickler von JavaScript-Apps kümmern müssen. Googles Framework AngularJS soll sie beim Erstellen von Applikationen unterstützen, die nur aus einer Seite bestehen.

Das Erstellen von Anwendungen in JavaScript gestaltet sich anspruchsvoll: Der Entwickler muss sich um das Binden von Daten, das Aufrufen von Services und das Validieren von Eingaben kümmern. Der Quellcode, der dabei entsteht, soll darüber hinaus auch überschau-, wart- und testbar sein. All das ist mit JavaScript zwar möglich, fordert dem Entwickler aber viel Disziplin ab und geht mit dem Schreiben großer Mengen ähnlicher Codestrecken einher.

JavaScript-Frameworks versprechen dafür Abhilfe. Eines davon ist AngularJS, das aus der Feder von Google stammt. Es zeichnet sich dadurch aus, dass es viele Aspekte moderner JavaScript-Anwendungen unterstützt und dabei die Kriterien Wartbarkeit und Testbarkeit in den Vordergrund stellt.

Artikelserie zu AngularJS

Zur Einführung in AngularJS beschreibt diese Artikelserie die Umsetzung einer einfachen Single Page Application (SPA). Sie soll dem Anwender die Möglichkeit geben, unter Verwendung eines mit JSON arbeitenden HTTP-Services Flüge zu buchen. Abbildung 1 veranschaulicht die vier Bereiche der SPA: Unter "Passagier" kann der Anwender nach einem Passagier suchen, indem er entweder dessen Nachnamen oder Nummer erfasst. Den gewünschten Passagier kann er anschließend auswählen. Unter "Flug" sucht der Anwender analog dazu einen Flug und wählt ihn aus. Filtern kann er entweder nach der Flugnummer oder nach dem Abflugs- und Zielflughafen. Im Bereich "Buchen" besteht die Möglichkeit, eine Buchung für den gewählten Passagier und Flug zu erstellen und unter "Meine Buchungen" kann der Anwender auf die von ihm gebuchten Flüge zugreifen.

Suchen und Auswählen eines Passagiers in der Beispielanwendung (Abb. 1)


Definition eines Moduls und Controllers

AngularJS strukturiert Anwendungen anhand von Modulen. Ähnlich wie Namespaces unter .NET kapselt ein Modul wiederverwendbare Programmteile und Konfigurationsinformationen. Zu Ersteren zählen Controller im Sinne des Musters MVC (Model View Controler). Controller haben in AngularJS die Aufgabe, Models bereitzustellen. Letztere lassen sich anschließend von einer View visualisieren.

Der folgende Codeausschnitt zeigt, wie der Entwickler ein Modul mit einem Controller bereitstellen kann.

var app = angular.module("Flug", []);


app.controller("FlugBuchenCtrl", function ($scope, $http, $q) {
$scope.vm = new FlugBuchenVM($scope, $http, $q);
});

Dazu nutzt er die Funktion module des globalen Objektes angular, das die von AngularJS bereitgestellten Konstrukte enthält. Damit die Funktion ein Modul einrichtet, übergibt der Entwickler den Namen des neuen Moduls und ein Array mit jenen Modulen, die in das neue Modul zu importieren sind. Durch diese Möglichkeit können Entwickler wiederkehrende Aufgaben auslagern.

Da im betrachteten Beispiel kein Modul (explizit) zu importieren ist, übergibt das betrachtete Beispiel lediglich ein leeres Array. Übergibt der Entwickler neben dem Namen des Moduls keine weiteren Argumente, erzeugt module kein neues Modul, sondern liefert ein bestehendes mit dem angegebenen Namen zurück – sofern ein solches existiert. (Genaugenommen wird in jedes neue Modul das Modul ng importiert, das die von AngularJS bereitgestellten Funktionen enthält. Das gilt auch, wenn ng, wie im gezeigten Beispiel, nicht explizit angegeben wird.)

Die Funktion module retourniert ein Objekt, welches das jeweilige Modul beschreibt. Das Beispiel nutzt die Funktion controller des Objekts, um einen neuen Controller einzurichten. Der erste Parameter ist der Name des Moduls. Beim zweiten handelt es sich um eine Funktion, die sich zum Ermitteln von Views nutzen lässt. Sie kann mit einer Action-Methode bei ASP.NET MVC verglichen werden.

Promises

Die JavaScript-Community verwendet Promises, um asynchronen Code wartbarer zu gestalten. Ein Promise repräsentiert eine asynchrone Funktion, deren Ergebnis vorliegt oder noch aussteht. Liefert eine asynchrone Funktion einen Promise zurück, kann der Aufrufer dessen Funktion then verwenden, um Funktionen als Callbacks zu registrieren. Die erste Funktion kommt zur Ausführung, wenn die asynchrone Funktion erfolgreich war; die zweite, wenn die asynchrone Funktion gescheitert ist:

someAsyncFunction().then(
function (rnd) { [...] },
// Success-Handler
function (error) { [...] }
// Error-Handler
);

Um die benötigten Objekte entgegenzunehmen, kommen in der Funktion Parameter zum Einsatz. Objekte werden auch als Abhängigkeiten (engl. Dependencies) bezeichnet. Beim Einsatz des Controllers übergibt nicht der Entwickler die Dependencies, sondern AngularJS – ein Vorgang der auch als Dependency Injection bekannt ist. Damit AngularJS die Abhängigkeiten injizieren kann, müssen die Namen der Parameter denen der zu injizierenden Abhängigkeiten (Services in AngularJS) entsprechen.

Im Beispiel kommen die Parameter $scope, $http und $q zum Einsatz. Der Parameter $scope zeigt auf ein Objekt, das Variablen, die in einem bestimmten Teil der Anwendung gültig sind, enthält. Die Aufgabe eines Controllers liegt im Bereitstellen von Informationen über dieses Objekt. Dabei handelt es sich um das Model im Sinne von MVC. Das Beispiel nutzt die dynamische Eigenschaft von JavaScript, um im Objekt $scope eine neue Eigenschaft vm einzurichten. Diese verweist auf das zu verwendende ViewModel. Dabei handelt es sich um ein Objekt von FlugBuchenVM.

Der Parameter $http bezeichnet einen Dienst, der im Lieferumfang von AngularJS enthalten ist und auf einfache Weise den Zugriff auf Ressourcen via HTTP erlaubt. Im Kontext von AngularJS werden Services als wiederverwendbare Objekte bezeichnet, die über Dependency Injection zu beziehen sind. $q ist ein Service, der sich zur Erzeugung von Promises heranziehen lässt und, wie der Name impliziert, von der populären JavaScript-Bibliothek Q inspiriert ist.

ViewModel und Datenbindung

Da AngularJS über die Namen der verwendeten Parameter auf die zu injizierenden Services schließt, sind Probleme beim Einsatz von Minification programmiert. Der Grund dafür ist, dass im Zuge der Code-Komprimierung für gewöhnlich die Namen von Variablen und Parametern durch kürzere Bezeichner ersetzt werden. Dies hat zur Folge, dass die für AngularJS benötigten Informationen verloren gehen. Um sie zu erhalten, kann der Entwickler sie in Form von Strings im Quellcode deponieren. Das folgende Code-Beispiel veranschaulicht dies.

app.controller("FlugBuchenCtrl", 
["$scope", "$http", "$q", function ($scope, $http, $q)
{
$scope.vm = new FlugBuchenVM($scope, $http, $q);
}]);

Statt einer Funktion übergibt es an den zweiten Parameter von controller ein Array, das aus den Namen der zu injizierenden Services besteht und an der letzten Stelle die vom Controller zu verwendende Funktion enthält. Der erste Eintrag des Arrays umfasst den Namen des Dienstes, der in den ersten Parameter der Funktion zu injizieren ist, der zweite Eintrag den Namen des Services, der in den zweiten Parameter der Funktion zu injizieren ist, und so weiter.

Deklaration des ViewModel

Das folgende ViewModel repräsentiert einen Flug:

function FlugVM(flug) {
this.Id = flug.Id;
this.Abflugort = flug.Abflugort;
this.Zielort = flug.Zielort;

if (typeof flug.Datum == "string") {
this.Datum = moment(flug.Datum).toDate();
}
else {
this.Datum = flug.Datum;
}
}

Ein JavaScript-Objekt, das seine Funktion von einem HTTP-Service erhalten hat, initialisiert den Flug. Falls die Eigenschaft Datum nicht bereits als Date vorliegt, wird sie mit der Funktion moment aus der freien Bibliothek moment.js in ein Date-Objekt umgewandelt.

Das ViewModel in Listing 1 repräsentiert die gesamte Anwendung. Es nimmt den aktuellen Scope sowie den HTTP-Service und den Q-Service von AngularJS entgegen und hinterlegt ihn für die spätere Verwendung in entsprechenden Eigenschaften. Daneben bietet es eine Eigenschaft fluege an, die auf ein Array mit den abgerufenen Flügen verweist. Die Eigenschaft selectedFlug verweist auf jenen Flug, den der Benutzer ausgewählt hat, und message enthält Informationen wie Fehler- oder Statusmeldungen, die dem Benutzer anzuzeigen sind. Die Funktion loadFluege simuliert das Laden von Flügen, indem es zwei hartcodierte Flüge zum Array fluege hinzufügt. Alternativ lässt sich das ViewModel so implementieren, dass sich die Funktion loadFluege unter Verwendung der Eigenschaften flugNummerFilter, flugVonFilter und flugNachFilter zum Abrufen von Flügen an einen HTTP-Service wendet.

Die Funktion selectFlug nimmt den Index eines Flugs des Arrays entgegen und platziert ihn in der Eigenschaft selectedFlug.

Datenbindung verwenden

Um ein Model beziehungsweise ein ViewModel, das ein Controller über seinen Scope bereitstellt, an eine View zu binden, ist Letztere mit dem Controller zu verknüpfen. Dazu verwendet der Entwickler das von AngularJS verwendete Attribut ng-app (Name des Moduls) und ng-controller (Name des Controllers).

Diese vom Framework bereitgestellten Attribute, die zudem mit auszuführenden JavaScript-Routinen verknüpft sind, werden als Direktiven bezeichnet. Innerhalb des Elements, das mit ng-controller versehen ist, kann der Entwickler auf den vom Controller bestückten Scope zugreifen:

<div ng-app="flug">
<div ng-controller="FlugBuchenCtrl">
[...]
</div>
</div>

Die Direktive ng-show legt fest, ob ein Element angezeigt werden soll. Wird der damit referenzierte Wert als true ausgewertet, zeigt die Anwendung das Element, auf das ng-show angewandt wurde, an – sonst nicht. In Listing 2 verweist ng-show auf die Eigenschaft vm.message. Das führt dazu, dass das entsprechende div-Element nur zu sehen ist, wenn das hinter vm stehende ViewModel über die Eigenschaft message eine Nachricht für den Benutzer enthält. Der Bindungs-Ausdruck

{{ vm.message }}

bewirkt, dass die Nachricht ebendort ausgegeben wird. Bei den darauffolgenden, über das Element Input beschriebenen Textfeldern kommt die Direktive ng-model zum Einsatz, um die Eigenschaften flugNummerFilter, flugVonFilter und flugNachFilter des ViewModels an die Textfelder zu binden. Dabei handelt es sich um eine bidirektionale Datenbindung, das heißt, Änderungen an den Eigenschaften werden ans Textfeld weitergegeben und vice versa. Die Direktive ng-click des darauffolgenden Buttons verknüpft den Click-Handler mit der Methode loadFluege des ViewModels. Dabei ist zu beachten, dass ng-click nicht nur den Namen der Funktion, sondern den gesamten Funktionsaufruf repräsentiert. Da es sich bei loadFluege um eine Funktion handelt, die keine Argumente erwartet, stellt man dem Funktionsnamen zwei runde Klammern nach. Eventuelle Parameter ließen sich, wie gewohnt, innerhalb dieser anfügen.

Weitere Direktiven

Neben ng-click bietet AngularJS weitere Direktiven, die es erlauben, Funktionen an JavaScript-Ereignisse zu binden. Darunter befinden sich zum Beispiel ng-change (Änderung des Werts eines Steuerelements), ng-blur (Steuerelement hat Fokus verloren), ng-focus (Steuerelement hat Fokus erhalten) oder ng-checked (Checkbox wurde aktiviert beziehungsweise deaktiviert). Einen Überblick über sämtliche Direktiven hält die Dokumentation von AngularJS bereit.

Die Tabelle in der zweiten Hälfte des Listing 2 verwendet die beschriebene Direktive ng-show. Sie legt fest, dass die Anwendung die Tabelle nur anzeigt, wenn das Array fluege mindestens einen Eintrag enthält. Statt des Größer-Symbols (>), das in HTML ein reserviertes Zeichen darstellt, kommt die HTML-Entität &gt; (gt für greater than) zum Einsatz.

Die Direktive ng-repeat wiederholt das tr-Element der Tabelle für jeden Eintrag im Array fluege des ViewModels. Der darin hinterlegte Ausdruck legt fest, dass der jeweils behandelte Flug in der Variable f abzulegen ist und dass sich der Index, den der Flug im Array einnimmt, über die Variable $index in Erfahrung bringen lässt.

Zusätzlich bewirkt die Direktive ng-class, dass das tr-Element mit der CSS-Klasse selected zu formatieren ist, wenn die ID des gerade behandelten Flug-Objekts der ID des Flug-Objekts in der Eigenschaft selectedFlug entspricht. Das hat zur Folge, dass die Anwendung das markierte Objekt hervorgehoben darstellt. Unter Verwendung von entsprechenden Ausdrücken bindet danach die Anwendung die Eigenschaften ID, Datum, Abflugort und Zielort des jeweiligen Fluges an den Inhalt der einzelnen Zellen.

Die Direktive ng-click verknüpft die Methode selectFlug des ViewModels mit dem Click-Ereignis eines Links mit der Beschriftung "Auswählen". Dabei wird festgelegt, dass der jeweilige Wert der Variable $index an selectFlug zu übergeben ist. Damit sich der Link in jedem Browser anklicken lässt, ist er mit einem href-Attribut auszustatten. Es hat im betrachteten Fall den Wert javascript:void(0). Dies bewirkt, dass keine Aktion ausgeführt wird, wodurch das Programm bei einem Klick lediglich die hinter dem Click-Handler stehende Funktion ausführt.

Fazit

AngularJS unterstützt die Entwicklung von SPAs, indem es unter anderem Datenbindung sowie eine Implementierung des Musters MVC bietet. Damit trägt es auch zur Schaffung wartbaren Quellcodes bei. Durch den zusätzlichen Einsatz von Dependency Injection lassen sich Komponenten, die für AngularJS entwickelt werden, zudem vergleichsweise einfach automatisiert testen. (jul)

Manfred Steyer
ist freiberuflicher Trainer und Berater bei IT-Visions sowie verantwortlich für den Fachbereich "Software Engineering" der Studienrichtung "IT und Wirtschaftsinformatik" an der FH CAMPUS 02 in Graz. In seinem Buch "Moderne Webanwendungen mit ASP.NET MVC und JavaScript-APIs" beschreibt er unter anderem, wie sich mit AngularJS Single Page Applications entwickeln lassen.

Listings

Listing 1: ViewModel für die gesamte Anwendung

function FlugBuchenVM(scope, http, q) {
var that = this;

this.fluege = new Array();

this.scope = scope;
this.q = q;
this.http = http;

this.selectedFlug = null;
this.message = "";

this.flugNummerFilter = "";
this.flugVonFilter = "";
this.flugNachFilter = "";

this.loadFluege = function () {

that.fluege.push(new FlugVM({
Id: 1,
Abflugort: "Graz",
Zielort: "Essen",
Datum: new Date().toISOString() }));

that.fluege.push(new FlugVM({
Id: 2,
Abflugort: "Essen",
Zielort: "Graz",
Datum: new Date().toISOString() }));
}

this.selectFlug = function (idx) {
var f = this.fluege[idx];
this.selectedFlug = f;
};

}

Listing 2: Einfache View

<div>

<div class="step-header">
<h2>Flug auswählen</h2>
</div>

<div ng-show="vm.message" class="message">
{{ vm.message }}
</div>

<div>Flugnummer</div>
<div><input ng-model="vm.flugNummerFilter" /></div>

<div>Von</div>
<div><input ng-model="vm.flugVonFilter" /></div>

<div>Nach</div>
<div><input ng-model="vm.flugNachFilter" /></div>

<div><input type="button" value="Suchen" ng-click="vm.loadFluege()" />
</div>

<div>

<table ng-show="vm.fluege.length &gt; 0">
<tr>
<th>Id</th>
<th>Abflugort</th>
<th>Zielort</th>
<th>Freie Plätze</th>
</tr>
<tr ng-repeat="f in vm.fluege track by $index"
ng-class="{ selected: f.Id == vm.selectedFlug.Id }">

<td>{{f.Id}}</td>
<td>{{f.Datum}}</td>
<td>{{f.Abflugort}}</td>
<td>{{f.Zielort}}</td>
<td><a href="javascript:void(0)"
ng-click="vm.selectFlug($index)">Auswählen</a></td>
</tr>
</table>


</div>

</div>