Mobile Web-Apps mit JavaScript

Data Binding mit Angular
Kommentare

Angular (http://angularjs.org/) ist ein Open-Source-Projekt, das unter MITLizenz in Version 1.0 zur Verfügung steht. Es beschreibt sich selbst als umfassendes Framework für die Entwicklung von JavaScript-Webapplikationen. Einen ausführlichen Überblick über Angular bietet der Developer Guide, siehe http://docs.angularjs.org/guide/dev_guide.overview.

In diesem Abschnitt wollen wir die Unterstützung für Two Way Data Binding von Angular nutzen, um unserer Controller-Logik und unser Modell an die Oberfläche unsere Beispiel-App anzubinden. Code, wie wir ihn in Listing 6.3 geschrieben haben, wird damit überflüssig. Als App-Entwickler können wir uns also in erster Linie um die Entwicklung der App-spezifischen Funktionen kümmern und müssen keine Zeit für Infrastrukturcode investieren. Im nächsten Kapitel werden wir dann Angular und dessen Unterstützung für Dependency Injection nutzen, um die Anbindung der Applikation an das Backend zu implementieren.

Scopes und Controller

Die Grundlage für Data Binding ist das Erkennen von Änderungen in einem Objekt. Dafür führt Angular so genannte Scopes ein. Ein Scope ist ein Daten-Container mit Properties, die gelesen, geschrieben und hinsichtlich Änderungen überwacht werden können. Die Methode $watch(watchExpression, callback) eines Scope-Objekts erlaubt es beispielsweise, auf Änderungen in den Daten eines Scopes zu reagieren. Im einfachsten Fall ist die Watch Expression der Name einer Property des Scope-Objekts. Sobald sich der Wert dieser Property ändert, wird die Funktion callback aufgerufen und kann auf diese Änderungen reagieren. Im allgemeinen Fall handelt es sich bei den Watch Expressions um an JavaScript-Syntax angelehnte Ausdrücke zum Lesen und Schreiben von Properties sowie zum Aufrufen von Funktionen. Scope-Objekte können von Angular an die Benutzeroberfläche einer Applikation gebunden und mit ihr synchronisiert werden. Die Aufgabe von Controllern in einer Angular-basierten Applikation ist es dann, diese Scope-Objekte um benötigte Properties und benötigtes Verhalten zu erweitern und den initialen Zustand eines Scope-Objekts herzustellen. Listing 6.4 zeigt die für die Verwendung mit Angular angepasste Version unseres Application Controllers aus dem Login-Beispiel. Der Konstruktorfunktion dieser Version des Controllers wird von Angular ein Scope-Objekt übergeben, welches dann initialisiert und um Funktionalität erweitert wird.

function ApplicationController($scope, navigate) {
  $scope.username = "";
  $scope.password = "";

  $scope.loginPossible = function() {
    return $scope.username && $scope.password;
  };

  $scope.login = function() {
    $scope.customer = {
      name: $scope.username
    };
    navigate("#welcomePage");
  };
}

Die Verbindung von Scope-Objekten mit Oberflächenelementen geschieht dann nach dem folgenden Prinzip: Angular registriert einerseits Listener für Felder in der Oberfläche und andererseits Listener für Properties im Scope-Objekt. Für die Properties im Scope-Objekt wird dabei die Funktion $watch verwendet. Wird ein Wert in der Oberfläche geändert, aktualisiert Angular die zugehörige Property im Scope-Objekt und umgekehrt. Wird ein Button in der Oberfläche geklickt, so ruft Angular die entsprechende Funktion im Scope-Objekt auf. Anschließend ruft Angular die Funktion $digest eines Scope-Objekts auf. Diese Funktion prüft alle Properties im Scope-Objekt, die mit $watch überwacht werden, auf Änderungen. Jede geänderte Property im Scope wird von Angular in die Oberfläche übertragen. Dass heißt, das DOM wird durch Angular entsprechend aktualisiert. Dieses Zusammenspiel von Views und Scope-Objekten in Angular wird in Abbildung 6.2 dargestellt.

Zusammenspiel von Views und Scopes in Angular

Abbildung 6.2: Zusammenspiel von Views und Scopes in Angular

Um Angular Controller mit Unit Tests testen zu können, stellt das Projekt eine eigene Bibliothek angular-mocks zur Verfügung, die zusammen mit dem Testframework Jasmine verwendet werden kann. Die Bibliothek stellt im Wesentlichen zwei globale Funktionen zur Verfügung:

  • module: Mithilfe dieser Funktion können Angular-Module geladen und Tests bereitgestellt werden. In der Regel wird module in der beforeEach-Funktion einer Jasmine Suite verwendet.
  • inject: Mithilfe dieser Funktion kann man einem Test einen Angular-Service per Dependency Injection übergeben, der dann im Test verwendet wird.

Listing 6.5 zeigt die Verwendung am Beispiel von ApplicationControllerSpec.

  describe('ApplicationController', function () {
    var $scope, $navigate;
  
    beforeEach(function () {
      module('rylc-controllers', function ($provide) {
        $provide.factory('$navigate', function () {
          $navigate = jasmine.createSpy();
          return $navigate;
        });
     });
     inject(function ($rootScope, $controller) {
       $scope = $rootScope.$new();
       $controller("rylc.ApplicationController",
                   {$scope:$scope});
     });
   });
 
   describe('loginPossible', function () {
     it('should ret. true when uname & passwd not empty',
     function () {
       $scope.username = "someUsername";
       $scope.password = "somePassword";
       expect($scope.loginPossible()).toBeTruthy();
     });
     // ...
   });
 
   describe('login', function () {
     it('should navigate to the welcomePage', function () {
       $scope.login();
       expect($navigate)
         .toHaveBeenCalledWith("#welcomePage");
     });
     // ...
   });
 
   // ...
 });

In der beforeEach-Methode laden wir zunächst mithilfe der Funktion module das Angular-Modul rylc-controller, welches den zu testenden Application Controller enthält (Zeile 5). Zusätzlich übergeben wir eine Callback-Methode, die von Angular den Build-in-Service $provide übergeben bekommt. Mit diesem Service überschreiben wir die Factory-Funktion für den Service $navigate vom jQuery-Mobile-Angular-Adapter mit einer Factory-Funktion, die einen Jasmine Spy erzeugt und zurückgibt (Zeile 6 bis 9). Zusätzlich merken wir uns den Spy in der Variable $navigate (Zeile 7), um in den nachfolgenden Tests überprüfen zu können, dass $navigate wie erwartet verwendet wird. In den Zeilen 11 bis 15 verwenden wir dann die Funktion inject, um uns die Build-in-Services $rootScope und $controller injizieren zu lassen. Mit $rootScope erzeugen wir ein neues Scope-Objekt (Zeile 12) und mit $controller veranlassen wir, dass unser Application Controller dieses Scope-Objekt initialisiert.

Die Zeilen 19 bis 24 zeigen dann einen Test, der prüft, dass die Funktion $scope.loginPossible den Wert true zurückgibt, wenn die Properties $scope.username und $scope.password einen Wert enthalten. In den Zeilen 29 bis 33 prüfen wir, dass $navigate mit dem Argument „#welcome“ aufgerufen wird, wenn man die Funktion $scope.login aufruft. (Bisher enthält die Funktion login ja noch keine Überprüfung der Anmeldedaten, sondern leitet immer zur Welcome Page weiter.) Natürlich enthält die Test Suite für ApplicationController noch diverse weitere Tests.

Hinweis

Die Funktion inject verwendet die String-Repräsentation der ihr übergebenen Callback-Funktion, um die Namen der Parameter und damit die Namen der Services zu ermitteln, die injiziert werden sollen. Dies funktioniert natürlich nur, solange kein Minifier den JavaScript-Code minifiziert. Für diesen Fall unterstützt inject eine weitere Aufruf-Semantik: inject([„service1“, „service2“, …, callback(service1, service2, …)]). Hierbei werden also die Namen der bereitzustellenden Services explizit angegeben. Da Testcode aber nicht minifiziert werden muss, können wir in Tests die kürzere und leichter lesbare Semantik verwenden.

[header=Templates]

Templates

Angular verwendet einen sehr allgemeinen Template-Mechanismus, über den unter anderem auch die Deklaration des Data Bindings für eine Page erfolgt. Die Dokumentation von Angular beschreibt Templates wie folgt: „An angular template is the declarative specification that, along with information from the model and controller, becomes the rendered view that a user sees in the browser. It is the static DOM, containing HTML, CSS, and angular-specific elements and angular-specific element attributes. The angular elements and attributes direct angular to add behavior and transform the template DOM into the dynamic view DOM.“ (Siehe http://docs.angularjs.org/guide/dev_guide.templates). Dieser Template-Mechanismus ist modular aufgebaut und basiert auf so genannten Directives. Eine Directive wird durch JavaScript-Code implementiert und ist einem HTML-Element, einem HTML-Attribut, einem Angular-spezifischen Element oder einem Angular-spezifischen Attribut zugeordnet. Zusätzlich wird eine besondere Syntax in Textknoten unterstützt. Ein Template besteht dann aus HTML- und Angular-spezifischem Markup und wird in einer Compile-Phase durch den HTML-Compiler von Angular in das DOM überführt, das vom Browser angezeigt wird. Das Kompilieren eines Templates wird normalerweise beim Start der Anwendung durchgeführt. Dazu ergänzt man im einfachsten Fall das Angular-Attribut ng-app am HTML-Wurzelelement html, siehe Listing 6.6.

<!DOCTYPE html>
<<html ng-app>
  ...
</html>

Angular traversiert dann den gesamten DOM-Baum und sucht darin nach Markup für die registrierten Directives. Für jede Verwendung wird der JavaScript-Code der Directive aufgerufen, der beliebige Operationen auf dem entsprechenden DOM-Element ausführen kann. Directives können auf Daten von Scope-Objekten zugreifen und verwenden dazu ebenfalls Angular Expressions. Manche Directives erzeugen dieses Scope-Objekt selber, alle anderen erben den Scope für die Auswertung von Expressions von ihrem Vater-Element. Listing 6.7 zeigt als Beispiel das Angular Template für die Login Page aus unserer Beispielapplikation.

 <div id="loginPage" data-role="page"
       ng-controller="ApplicationController">
    <div data-role="header">
      <h1>RYLC - Login</h1>
    </div>
    <div data-role="content">
      <form data-ajax="false" ng-submit="login()">
        <div data-role="fieldcontain">
          <label for="loginPage_uname">Benutzername</label>
         <input type="text" id="loginPage_uname"
                ng-model="username">
       </div>
       <div data-role="fieldcontain">
         <label for="loginPage_passwd">Passwort</label>
         <input type="password" id="loginPage_passwd"
                ng-model="password">
       </div>
       <input type="submit" class="login" value="Login"
              ng-disabled="!loginPossible()">
     </form>
   </div>
 </div>

Im Vergleich mit Listing 5.6 und 5.7 aus dem letzten Kapitel ergeben sich folgende Änderungen: In Zeile 2 wird das HTML-Element div um das Angular-Attribut ng-controller=“ApplicationController“ ergänzt. Dadurch wird ein Scope-Objekt erzeugt und von Angular als Argument an die Konstruktorfunktion von ApplicationController übergeben, welche das Scope-Objekt daraufhin initialisiert, siehe Listing 6.4. Dieses Scope-Objekt wird dann an alle Angular Directives vererbt, die innerhalb der Page verwendet werden. In Zeile 7 wird das form-Element um das Angular-Attribut ng-submit=“login()“ ergänzt. Dadurch wird die Funktion login des Scope-Objekts an das Form Submit Event dieser Form gebunden und somit aufgerufen, wenn das Formular abgeschickt wird. In Zeile 11 wird das HTML-Element input um das Angular-Attribut ng-model=“username“ ergänzt. Dadurch wird das Input-Element an die Property username des Scope-Objekts gebunden. In Zeile 16 geschieht selbiges für das Passwort-Input-Element und die Scope-Objekt-Property password. In Zeile 19 schließlich wird der Wert des Angular-Attributs ng-disabled an die Angular Expression !loginPossible() gebunden und so die disabled-Eigenschaft des Elements gesteuert. Damit ist die Login Page an den Application Controller, genauer an das zugehörige Scope-Objekt angebunden. Eingaben in den Input-Elementen für Benutzername und Passwort werden mit den Properties username und password synchronisiert, der Enabled-Zustand des Login-Button wird mit dem Rückgabewert der Funktion loginPossible synchronisiert und ein Tap auf den Login-Button führt die Funktion login aus. Und all dies geschieht rein deklarativ, es ist kein zusätzlicher JavaScript-Code notwendig. Abbildung 6.3 gibt einen Überblick über das Binding von View und Scope in unserem Beispiel.

Data Binding zwischen View und Scope

Abbildung 6.3: Data Binding zwischen View und Scope

Integration von Angular und jQuery Mobile

Leider sind wir mit der Anbindung der Benutzeroberfläche an unsere Controller-Logik noch nicht ganz am Ziel. Wir verwenden nämlich zwei Frameworks, die beide das DOM im Browser manipulieren: Angular tut dies, wenn es die Markup-Templates kompiliert und so das endgültige DOM für den Browser herstellt und wenn es auf Änderungen im Modell reagiert und basierend darauf die View aktualisiert. Und jQuery Mobile tut dies, wenn es das Markup einer Page auswertet und mithilfe von Progressive Enhancement das optimale DOM für den aktuellen mobilen Browser erzeugt, siehe Abschnitt 5.2 in Kapitel 5. Diese konkurrierenden DOM-Manipulationen müssen koordiniert werden. Genau dies leistet der jQuery-Mobile-Angular-Adapter von Tobias Bosch. Der Adapter steht als Open-Source-Projekt unter MIT-Lizenz in GitHub zur Verfügung, siehe https://github.com/tigbro/jquery-mobile-angular-adapter. Zusätzlich zu der Koordination der DOM-Manipulationen von Angular und jQuery Mobile stellt der Adapter noch einige Angular Directives und so genannte Services zur Verfügung, die bei der Entwicklung mobiler Webapplikationen nützlich sind. Beispielsweise liefert der Adapter einen Angular Service $navigate, der die Navigation zwischen jQuery Mobile Pages ermöglicht und damit unsere navigate-Funktion aus Listing 6.2 ersetzt.

Die Verwendung des Adapters gestaltet sich denkbar einfach. Es muss nur das zugehörige Skript nach denen von jQuery, jQuery Mobile und Angular in die HTML-Seite eingebunden werden. Wir verwenden den Adapter in unserer Beispielapplikation und werden im Rest des Buches, insbesondere in Kapitel 8, einige ausgewählte Erweiterungen vorstellen.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -