AngularJS Tutorial im heißen Handtuch – Teil 1

Auch wenn sich die Zeit von Version 1.x von AngularJS mit dem Release von Version 2 in Zukunft dem Ende nähert, ist der Einsatz gerade in Kombination mit dem Style-Guide von John Papa noch immer eine lohnende Sache. Neben der Aussage, dass der Support von AngularJS 1 erst dann eingestellt wird, wenn der Traffic auf angular.io (Angular 2) den von angularjs.org (Angular 1) übersteigt, sich also mehr Entwickler für Angular 2 interessieren, existiert ein offizieller Upgrade-Pfad von Version 1.x auf Version 2. Bis Angular 2 sämtliche Kinderkrankheiten verloren hat, wird noch etwas Zeit vergehen, in der die meisten Entwickler AngularJS (V.1) vertrauen werden. Außerdem ist allein die schiere Zahl von bestehenden AngularJS-Applikationen Grund genug, sich diesem Thema zu widmen und sich im womöglich letzten AngularJS-Tutorial mit einem wirklich tollen Framework zu beschäftigen.

AngularJS, gemeinhin kurz Angular, ist eines der wohl populärsten JavaScript-Frameworks für clientseitige Web-Applikationen im Kontext von Single-Page-Applications (SPAs). Das liegt wohl vor allem daran, dass Angular von Google entwickelt und tatkräftig promotet wurde. Zwar mag dies ein Kritikpunkt sein, aber nichtsdestotrotz verdient Angular den Zuspruch einer großen Entwickler-Community. Denn Angular beinhaltet viele etablierte Entwurfsmuster der Softwaretechnik wie z.B. Model-View-ViewModel (MVVM), Two-Way Data Binding und Dependency Injection (DI) und besitzt dank Google im Rücken die Sicherheit einer kontinuierlichen Weiterentwicklung.

Die genannten Entwurfsmuster und Komponenten von Angular werden innerhalb dieses zweiteiligen Artikels in Form einer kleinen Beispielsapplikation erklärt. Hinter den drei verschiedenen Views verbirgt sich die Anbindung der Webapplikation an einen auf Node basierenden REST-Server, der wiederum mit einer MongoDB (NoSQL-Datenbank) verbunden ist.

Innerhalb der Main-View befindet sich eine Tabelle von Superhelden, definiert durch ihren Vor- und Nachnamen, ihrem Alias und einer ID. Die dargestellten Superhelden werden direkt aus der angebundenen Datenbank gelesen, sobald die View über die Adressleiste oder die Navigationsleiste geöffnet wird. Des Weiteren ist es möglich, der Datenbank über zwei verschiedene Form-Elemente innerhalb der View neue Superhelden hinzuzufügen bzw. einzelne anhand ihrer ID zu entfernen.

Im ersten Teil des AngularJS Tutorials werden die Grundlagen von Angular erläutert. Dazu zählen Module, das Bootstrapping von Angular, Controller, das Data Binding, Templates und einzelne Directives. Damit sollte ein grundlegendes Verständnis für weiterführende Konzepte wie Services, Data-Services, Promises und Event-Handling geschaffen sein, die im zweiten Teil des Tutorials erläutert werden.

HotTowel als Ausgangspunkt

(Hinweis: Mit folgendem Repository kann direkt zu Abschnitt „Endlich zum Frontend“ gesprungen werden, vorausgesetzt, eine MongoDB ist installiert.)

Um direkt mit der Programmierung der eigenen Angular-Webapplikation zu beginnen, ohne dabei lange mit Konfigurationen und dem Schreiben von Build-Scripts beschäftigt zu sein, ist es ratsam, einen der vielen Yeoman-Generatoren zu verwenden. Dieses Tutorial basiert auf dem HotTowel-Generator von John Papa. Weitere Informationen dazu lassen sich in dem Blog-Artikel „Von 0 auf 100 mit Yeoman” von Christoph Wolfes finden.

HotTowel beinhaltet bereits eine Vielzahl von Logging, Debugging und Build-Tools und Scripten, die einem das Leben bei der Entwicklung und Auslieferung einer Webapplikation das Leben sehr viel einfacher machen. Da sich dieser Artikel jedoch primär um Angular dreht, wird auf diese nicht im Detail eingegangen. Stattdessen können genauere Informationen sowie die Installation des HotTowel-Generators auf der entsprechenden Github-Seite eingesehen werden.

Aufräumen angesagt

Wurde sich an den QuickStart-Abschnitt des HotTowel-Generators gehalten, liegt nun bereits eine fertige Angular-Web-App vor, die mit $ gulp serve-dev bzw. $ gulp serve-build auf einem lokalen Node-Server gestartet werden kann. Diese Applikation besteht aus zwei Views, in die Widgets integriert werden können. Einer Admin-View, die bis auf ein Todo-Widget leer ist, und einer Dashboard-View, die auf Daten des integrierten Node-Servers zugreift. Der Node-Server verhält sich hierbei wie ein REST-Server, der Mock-Daten zurückgibt. Die Mock-Daten befinden sich in der Datei /src/server/data.js, die getrost gelöscht werden kann, da die zukünftigen Daten aus einer tatsächlichen Datenbank stammen sollen.
Weiterhin können die Ordner /src/client/app/admin/ und /src/client/app/dashboard/ inklusive aller enthaltenen Dateien gelöscht werden, da sie im Zuge des Tutorials durch zwei neue Ordner ersetzt werden.

Anschließend liegt folgende Struktur der Applikation vor:

src/
 |__ client/
        |__ app/
               |__ blocks/
               |__ core/
               |__ layout/
               |__ widgets/
               |__ app.module.js
 |__ server/
        |__ utils/
        |__ app.js
        |__ routes.js
        |__ favicon.ico

REST-Server mit Datenbankanbindung

Bevor die eigentliche Programmierung des Frontends beginnen kann, müssen noch ein paar Konfigurationen des Backends durchgeführt werden. Zuerst wird eine NoSQL-Datenbank benötigt, MongoDB ist dabei eine gute Wahl. Diese ist schnell installiert. Über das Terminal wird durch den Befehl $ npm install mongoose --save Mongoose standardmäßig zu unserem Projekt hinzugefügt (siehe entsprechender Eintrag in package.json). Mongoose benötigen wir für den einfachen Zugriff auf unsere Datenbank. Jetzt müssen innerhalb unseres Projekts noch die beiden Dateien /src/server/app.js und /src/server/routes.js geändert und die neue Datei /src/server/utils/models/hero.js erstellt werden:

Ergänzen von app.js um:

// app.js
...
var four0four = require('./utils/404')();

// new
var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017');

// old
var environment = process.env.NODE_ENV;
...

Ersetzen von routes.js durch:

// routes.js
var router = require('express').Router();
var four0four = require('./utils/404')();
var Hero = require('./utils/models/hero'); // mongoose model

router.get('/watchmen', getWatchmen); // <host-ip>:<port>/api/watchmen
router.post('/watchmen', postWatchmen);
router.get('/watchmen/:heroId', getWatchmenById);
router.put('/watchmen/:heroId', updateWatchmenById);
router.delete('/watchmen/:heroId', deleteWatchmenById);
router.get('/*', four0four.notFoundMiddleware);

module.exports = router;

function getWatchmen(req, res, next) {
  Hero.find(function (err, heroes) {
    if (err) {
      res.send(err);
    }
    else {
      res.json(heroes);
    }
  });
}

function postWatchmen(req, res, next) {
  var hero = new Hero();
  hero.firstName = req.body.firstName;
  hero.lastName = req.body.lastName;
  hero.alias = req.body.alias;

  hero.save(function (err) {
    if (err) {
      res.send(err);
    }
    else {
      res.json({ message: 'Hero ' + req.body.alias + ' created!' });
    }
  });
}

function getWatchmenById(req, res, next) {
  Hero.findById(req.params.heroId, function (err, hero) {
    if (err) {
      res.send(err);
    }
    else {
      res.json(hero);
    }
  });
}

function updateWatchmenById(req, res, next) {
  Hero.findById(req.params.heroId, function (err, hero) {
    if (err) {
      res.send(err);
    }
    else {
      hero.firstName = req.body.firstName;
      hero.lastName = req.body.lastName;
      hero.alias = req.body.alias;
    }
    hero.save(function (err) {
      if (err) {
        res.send(err);
      }
      else {
        res.json({ message: 'Hero ' + req.body.alias + ' updated!' });
      }
    });
  });
}

function deleteWatchmenById(req, res, next) {
  Hero.remove({
    _id: req.params.heroId
  }, function (err, hero) {
    if (err) {
      res.send(err);
    }
    else {
      res.json({ message: 'Hero with id ' + req.params.heroId + ' successfully deleted!' });
    }
  });
}

Erstellen der Datei /src/server/utils/models/hero.js:

var mongoose = require('mongoose');
var Schema = mongoose.Schema;

var HeroSchema = new Schema({
  firstName: String,
  lastName: String,
  alias: String
});

module.exports = mongoose.model('Hero', HeroSchema);

Somit kann der Node-Server mit einer Datenbank verbunden werden, auf die mittels REST-API zugegriffen werden kann. Über die API können einzelne Helden gelöscht, geändert oder neu hinzugefügt werden und die Anzeige aller oder einzelner Helden ist möglich (Hinweis: Es wird davon ausgegangen, dass die Datenbank läuft und auf Port 27017 hört. Änderungen bzgl. der Datenbank sind in der Datei app.js möglich).

Um genau zu verstehen, was in den soeben absolvierten Schritten passiert, lohnt sich definitiv ein Blick auf diesen Artikel.

Endlich zum Frontend

Somit ist die Konfiguration abgeschlossen und alle Voraussetzungen erfüllt, so dass mit der Entwicklung der GUI begonnen werden kann. Da vor zwei Abschnitten die beiden Views gelöscht worden sind und lediglich das Grundgerüst steht, müssen neue Views angelegt werden. Hier beginnt der Einstieg mit Angular.

Es sollen zwei neue Views angelegt werden. Eine simple Welcome-View mit einer Nachricht und eine zweite View, die wie in der Einleitung beschrieben reagiert und den Zugriff des Frontends auf die REST-API verdeutlicht. Die momentane Modul-Abhängigkeits-Struktur der Frontend-Applikation sieht wie folgt aus:

app →[
  app.layout →[
    app.core
  ],
  app.widgets,
  app.core →[
    ngAnimate,
    ngSanitize,
    ui.router,
    blocks.exception,
    blocks.logger,
    blocks.router
  ]
]

Was Module und die folgenden Angular-Komponenten sind, wird in den nächsten Abschnitten begleitend zum eigentlichen Tutorial grundlegend erklärt. Doch kurz eine Erläuterung zum Grundgerüst der bestehenden Anwendung.
Das Core-Modul enthält Services, Provider und Konfigurationen, die über die komplette Anwendung hinweg geteilt werden. Dazu zählen unter anderem Daten-Services, angularspezifische Module (gekennzeichnet durch ng-Präfix), Router- und Block-Module. Die Block-Module sind wiederverwendbare Code-Blöcke wie z.B Logger, Exceptions und Router. Das Widgets-Modul enthält Templates und Directives zur Verwendung von Widgets innerhalb der Applikation.

Was sind Module?

Module können als Container für die einzelnen Teile der Applikation verstanden werden, also für Controller, Services, Filter, Directives etc. In der Beispielapplikation wird in der Datei app.module.js das Modul appdefiniert. Dieses Modul kann als Main-Modul betrachtet werden, es besitzt Abhängigkeiten zu allen anderen Modulen der Applikationen und ist Einstiegspunkt für das Bootstrapping innerhalb der index.html.

Die Definition (Setter) des Moduls mit seinen Abhängigkeiten sieht wie folgt aus:

// app.module.js
angular.module('app', [
    'app.core',
    'app.widgets'
    'app.layout'
]);

Die Module innerhalb der eckigen Klammern sind Abhängigkeiten von app und müssen vor diesem geladen werden. Der Befehl angular.module(‘app’,[]); erschafft das Modul appund kann als Setter verstanden werden, wohingegen angular.module(‘app’); als Getter für ein erschaffenes Modul dient. Es sorgt für Übersichtlichkeit innerhalb der Projektstruktur, Konfigurationen des Moduls in gesonderten Dateien vorzunehmen. Dies geschieht über die entsprechende Getter-Funktion. Ebenso lassen sich so Komponenten wie Controller, Directives, Services etc. an einem Modul registrieren.

Was ist Bootstrapping?

Als Bootstrapping wird der Initialisierungsprozess von Angular innerhalb einer einfachen HTML-Seite verstanden. Mit Angular lassen sich Single Page Applications (kurz SPAs) bauen. Das bedeutet, es wird nicht für jede View eine neue HTML-Seite geladen. Stattdessen wird das DOM einer einzigen Seite dynamisch verändert, um Veränderungen an der View herbeizuführen.
Um Angular innerhalb einer HTML-Seite, für gewöhnlich der index.html, zu bootstrappen, sind folgende Schritte notwendig:

1. AngularJS mittels Script-Tag in HTML einbinden.
2. Eigene JS-Dateien (Module, Controller etc.) mittels Script-Tag einbinden.
3. ng-app=”app” innerhalb eines HTML-Tags ist Startpunkt für das Modul app.

In der Beispielapplikation sieht das wie folgt aus:

<!DOCTYPE html>
<html ng-app="app">

<head> ... </head>
...
<body>
  ...
  <script src="/bower_components/angular/angular.js"></script>
  ...
  <script src="/src/client/app/app.module.js"></script>
  ...
</body>

</html>

Ist Angular als Skript auf einer Seite eingebunden, sucht es, nachdem das DOMContentLoaded Event gefeuert wurde, automatisch nach der Directive ngApp (Hinweis: Mehr zu Directives gleich: Wichtig in diesem Kontext ist, dass sich ngApp auf <html ng-app="app"> innerhalb der HTML-Seite bezieht). Anschließend wird das entsprechende Modul geladen und der Application Injector erschaffen. Da die Directive ngApp als Wurzel für die Kompilierung behandelt wird, ist es möglich, Angular auch nur in Teilabschnitte einer HTML-Seite zu integrieren. Bei Angular bezeichnet man mit Kompilierung das Anbinden von Directives an die HTML-Seite.

Es ist ratsam, Skripte erst am Ende der HTML-Seite zu laden, um den Seitenaufbau nicht unnötig zu verlängern. In dem Beispiel wird vorausgesetzt, dass angular.js und app.module.js in entsprechender Ordnerstruktur liegen (Hinweis: HotTowel stellt ein Build-Script bereit, das den Vorgang des Einbindens automatisiert).

Was sind Directives?

Directives können als Marker auf einem DOM-Element (Attribut, Elementname, Kommentar, CSS-Klasse) gesehen werden, die dem HTML-Compiler von Angular mitteilen, dem DOM-Element bestimmtes Verhalten zuzuordnen oder dieses gar komplett zu verändern. Directives sind beispielsweise ngApp, ngRepeat, ngBind etc.

Da Angular die Element-Tags und Attributnamen während des Kompiliervorgangs in Camel-Case umformt, bezieht man sich bei Directives gemeinhin auf den normalisierten Namen. So wird aus <html ng-app="app"> z.B. ngApp.

Der Einstiegspunkt index.html

Wie schon im Abschnitt “Was ist Bootstrapping?” erläutert, besitzt unsere Beispielapplikation eine /src/client/index.html als Einstiegspunkt. Der HotTowel-Generator kümmert sich dabei dank vordefinierter Build-Scripts automatisch um das Einbinden von notwendigen JavaScript-, CSS- und HTML-Dateien, wenn sie sich im /src/client/app-Ordner unseres Projekts befinden ($ gulp inject).

Wie im unteren Code-Abschnitt zu sehen, ist unsere Applikation durch die Directive ng-app=”app” für die komplette HTML-Seite verantwortlich. ng-include=”’app/layout/shell.html’” bindet externe HTML-Seiten relativ zum Pfad der aktuellen Seite ein und ng-show=”showSplash” zeigt oder versteckt den eigenen Block abhängig vom boolean-Wert von “showSplash”. Diese Variable wird in der Datei /src/client/app/layout/shell.controller.js global definiert. Ein Splash Screen ist ein Platzhalter während des Ladevorgangs einer Anwendung.

<!DOCTYPE html>
<html ng-app="app">
...
<body>
  <div>
    <div ng-include="'app/layout/shell.html'"></div>
    <div id="splash-page" ng-show="showSplash">
      <div class="page-splash">
        <div class="page-splash-message">
          AngularJS Tutorial
        </div>
        <div class="progress progress-striped active page-progress-bar">
          <div class="bar"></div>
        </div>
      </div>
    </div>
  </div>
<!-- java script files -->
</body>
</html>

Letztlich ist die index.html inklusive aller eingebundenen Seiten das Grundgerüst der Beispielapplikation mit Navigationsleiste, Splash Screen und allgemeinem Layout, in das eigene Views integriert werden können.

Das erste eigene Modul

Für die einfache Welcome-View wird zunächst ein neues Modul app.welcome in der Datei /src/client/app/welcome/welcome.module.js erstellt.

// welcome.module.js
angular.module('app.welcome', [
    'app.core',
    'app.widgets'
  ]);

Das Modul hängt von app.core und app.widgets ab, die beide vor app.welcome geladen werden müssen, und kann somit auf deren Services etc. wie z.B. den Logger zugreifen.
An diesem Modul können wir nun unseren ersten eigenen Controller registrieren. Doch zuvor eine kleine Erklärung der einzelnen Komponenten des Model-View-ViewModel-Entwurfsmusters von Angular. Diese sind Controller (ViewModel), Scopes (Data-Model) und Templates (View).

Was sind Controller?

Controller dienen, wie der Name schon sagt, zur Datensteuerung. Innerhalb des MVVM-Entwurfsmusters übernehmen Controller die Rolle des ViewModels und besitzen eine bidirektionale Bindung zur View. Sie besitzen einen eigenen Scope (Data-Model), der bei der Erschaffung des Controllers erzeugt wird. Durch den injizierbaren Service $scope können Variablen des Controllers an die View gebunden werden. Wird ein Controller mit controllerAs-Syntax erzeugt, dann wird der Controller an einen bestimmten Wert innerhalb seines Scopes gebunden. Die Injizierung von $scope in den Controller ist dann nicht mehr notwendig. Was genau mit dieser Syntax gemeint ist, wird noch im Abschnitt „Routing mit AngularUI Router“ beschrieben.

// MVVM-Example
// Example controller

angular.module('mvvm.example.module')
  .controller('MVVMExampleController', ['scope', function($scope) {
    $scope.names = ['Hans', 'Heinz', 'Horst'];
  }];

Was sind Templates?

Bei Angular sind Templates HTML-Seiten mit angularspezifischen Elementen und Attributen. Sie repräsentieren die dynamische View der Applikation und stehen über den Scope in direkter Verbindung zum Controller. Ein Template kann folgende Angular-Elemente enthalten:

  • Directives: Attribut oder Element, das existierendes DOM-Element erweitert oder wiederverwendbares DOM-Element repräsentiert.
  • Markup: Doppelt geschwungende Klammern {{}}, um Expressions an Elemente zu binden.
  • Filter: Datenformatierung für die View.
  • Form controls: Validierung von Benutzereingaben.

Das folgende Beispiel enthält eine Expression und zwei Directives: ngController, zur Bindung des Controllers an das Template, und ngRepeat, das das DOM-Element für jeden Eintrag innerhalb des Arrays $scope.names wiederholt, das oben im MVVMExampleController definiert wurde, wiederholt.

<!-- MVVM-Example -->
<!-- Example template -->

<!DOCTYPE html>
<html ng-app="mvvm.example.module">
  <head>...</head>
  <body ng-controller="MVVMExampleController">
    <ul>
      <!-- runs over all objects as <object> in <array> -->
      <li ng-repeat="name in names">{{name}}</li>
    </ul>
  </body>
</html>

Was sind Scopes?

Scopes sind das Daten-Modell zwischen Controllern (ViewModel) und den Templates. Jede Angular-Applikation besitzt genau einen Root Scope und beliebig viele Child Scopes. Der Root Scope beginnt nach der ngApp Directive und umfasst die komplette Applikation. Child Scopes sind hierarchisch angeordnet und werden durch bestimmte Directives wie z.B. ngController oder ngRepeat innerhalb der View erschaffen. Angular unterstützt das Two-Way Data Binding, d.h. Veränderungen am Scope wirken sich gleichermaßen auf die View und den Controler aus. Änderungen können dabei z.B. per Benutzereingaben über die View oder programmatisch im Controller z.B. als Reaktion auf Events, erfolgen.

Das folgende Beispiel zeigt den resultierenden DOM nach Ausführen von ngRepeat. Es besteht aus drei verschiedenen Scope-Ebenen: Einem Root Scope und vier Child Scopes, von denen drei Scopes von ngRepeat unterhalb des Scopes von ngController liegen.

<!DOCTYPE html>
<html ng-app="mvvm.example.module" class="ng-scope"> <!-- Root Scope -->
  <head>...</head>
  <body ng-controller="MVVMExampleController" class="ng-scope ng-binding"> <!-- Ctrl Scope -->
    <ul>
      <!-- ngRepeat: name in names -->
      <li ng-repeat="name in names" class="ng-scope ng-binding"> <!-- Repeat Scope -->
        Hans <!-- first element of the array names -->
      </li>
      <li ng-repeat="name in names" class="ng-scope ng-binding"> <!-- Repeat Scope -->
        Heinz <!-- second element of names -->
      </li>
      <li ng-repeat="name in names" class="ng-scope ng-binding"> <!-- Repeat Scope -->
        Horst <!-- third element of names -->
      </li>
    </ul>
  </body>
</html>

Der erste eigene Controller

Zurück zur Beispielapplikation. Nachdem nun klar ist, wie das Data Binding von Angular funktioniert, wird ein Controller in der Datei /src/client/app/welcome/welcome.controller.js folgendermaßen erstellt.

// welcome.controller.js
// register the controller on the module
angular
    .module('app.welcome')
    .controller('WelcomeController', ['logger', WelcomeController]);

  // annotation for minify which rewrites variables
  /* @ngInject */ 
  function WelcomeController(logger) {
    // bind controller to vm for consistency reasons
    var vm = this;
    vm.title = 'Welcome';
 
    activate();

    function activate() {
      // display message on view activation
      logger.info('Activated Welcome View');
    }
  }

Zuerst wird der Controller am Modul registriert. Die controller method erhält als Parameter einen String, den Namen des Controllers, und ein Array bestehend aus zu injizierenden Services und einer Controller-Funktion.

Die Annotation /* @ngInject */ gehört zu ng-annotate. Gemeinhin wird der Code für den Produktionseinsatz minifiziert, wobei Variablen umgeschrieben werden, was zu Problemen bei Dependency Injection führt. Durch Angabe der Annotation und Verwendung von ng-annotate innerhalb eines Build-Scripts wird dieses Problem vermieden.
Durch controllerAs-Syntax ist es nicht notwendig, $scope in den Controller zu injizieren. Stattdessen wird this innerhalb des Controllers an eine Variable (z.B. vm für ViewModel) gebunden, was zu besserer Lesbarkeit führt.

/* avoid */
function CustomController($scope) {
    $scope.name = {};
    $scope.sendMessage = function() { };
}

Durch controllerAs-Syntax lässt sich der obere Code vermeiden und stattdessen wie folgt schreiben:

/* recommended to avoid $scope injection and for readability*/
function CustomController() {
    var vm = this; 
    vm.name = {};
    vm.sendMessage = function() { };
}

Die activate()-Funktion enthält die Start-Up-Logik eines Controllers. Dies dient der Übersichtlichkeit und führt zu besserer Testbarkeit des Controllers.
Controller sollten möglichst wenig Logik enthalten. Stattdessen sollte Logik in Services ausgelagert werden, damit sie über mehrere Controller hinweg geteilt werden kann. Dadurch werden Abhängigkeiten und Implementierungsdetails im Controller reduziert und der Controller auf seinen Anwendungsbereich, die Datenkontrolle, fokussiert.

Was ist Dependency-Injection?

Dependency Injection, kurz DI, ist ein Software-Entwurfsmuster, das sich um das Auflösen von Abhängigkeiten zwischen einzelnen Komponenten kümmert. Angulars Injector Subsystem ist in der Lage, Komponenten zu erschaffen, ihre Abhängigkeiten aufzulösen und sie anderen Komponenten bereitzustellen.
Für jede Angular-Anwendung wird während des Bootstrapping-Vorgangs automatisch ein Injector erschaffen, der sich um DI kümmert.

Die View zum Controller

Es liegt innerhalb der Beispielapplikation nun ein Modul mit zugehörigem Controller vor, aber es gibt noch keine View, die vom Controller manipuliert werden kann. Diese wird als nächstes in der Datei /src/client/app/welcome/welcome.html erstellt. Angezeigt werden soll ein einfaches Widget mit einem Titel und einer Textnachricht.

Die Nutzung von Widgets wird durch das von HotTowel vordefinierte Widget-Modul bereitgestellt. Des Weiteren wird Bootstrap zur Gestaltung verwendet. Das Template, also die HTML-Seite, das zum WelcomeController gehört, besteht aus folgenden Zeilen:

<!-- /src/client/app/welcome/welcome.html -->
<section class="mainbar">
  <section class="matter">
    <div class="container">
      <div class="row">
        <div class="col-md-6">
        <div class="widget wviolet">
          <div ht-widget-header title="{{vm.title}}"></div>
          <div class="widget-content user">
            <h3>
              <div>Welcome to AngularJS Tutorial!</div>
              <div>Click on Watchmen to add or delete Heroes from the database.</div>
            </h3>
          </div>
          <div class="widget-foot">
            <div class="clearfix"></div>
          </div>
        </div>
      </div>
      </div>
    </div>
  </section>
</section>

Die classes container, row und col-md-6 gehören zu Bootstrap, die classes mit widget-Präfix zum Widget-Modul. Das Data Binding zwischen Controller und View befindet sich in folgender Zeile: <div ht-widget-header title="{{vm.title}}"></div>. Die Expression beinhaltet das Attribut vm.title, das im WelcomeController definiert und den einfachen String Welcome besitzt.

function WelcomeController(logger) {
    // bind controller to vm for consistency reasons
    var vm = this;
    vm.title = 'Welcome';
    . . .           

Dieses Beispiel ist sehr grundlegend und zeigt ein einseitiges Data Binding vom Controller zur View. In späteren Abschnitten wird das Two-Way Data Binding vorgestellt, bei dem Nutzereingaben Daten im Controller verändern und umgekehrt.

Was zum jetzigen Zeitpunkt noch unklar ist, ist die Frage, woher Angular weiß, dass dieses HTML-Template in die Applikation mit eingebunden werden soll und welcher Controller dafür zuständig ist. Schließlich befindet es sich nicht innerhalb unseres Einstiegspunkts, der index.html.

Normalerweise werden Controller durch ng-controller-Directive in eine HTML-Seite eingebunden und somit der Scope des jeweiligen Controllers festgelegt. Die sieht folgendermaßen aus:

...
<body ng-app="customApp">
  <div ng-controller="customCtrl">
  <!-- some HTML code -->
  </div>  <!-- end of controller scope -->
</body>
</html>

Angular-Anwendungen sind SPA und theoretisch könnten sämtliche Komponenten (Controller, Directives etc.) von Angular innerhalb einer HTML-Seite eingebunden werden, was allerdings sehr schnell sehr unübersichtlich wird. Um Modularität zu erzeugen und somit einzelne logische Komponenten der Anwendung zusammenzufassen, ist ein anderer Ansatz wünschenswert.
Da Angular-Anwendungen im Web-Browser laufen, über den gemeinhin auf verschiedene Seiten über eine URL zugegriffen wird, ist es naheliegend, Abschnitte der Angular-Anwendung ebenfalls wie statische Pages zu behandeln und zu strukturieren. Abhilfe schafft dabei das Routing mit AngularUI Router. Routing ist vergleichbar mit dem Navigieren zwischen Pages von statischen Seiten über die Addressleiste des Browsers.

Routing mit AngularUI Router

Die Beispielapplikation bedient sich für das Routing bei dem Modul ui.router (siehe /src/client/app/core/core.module.js). Mit Hilfe dieses Moduls können Zustände definiert werden, die bei Betreten einer bestimmten URL aktiviert werden.

Der HotTowel-Generator stellt das Routing bereits standardmäßig zur Verfügung und definiert dafür den Provider routerHelper. Um auf diesen zuzugreifen und einen Zustand für die Welcome-View zu erstellen, wird die Datei /src/client/app/welcome/welcome.route.js erstellt:

// welcome.route.js
angular
    .module('app.welcome')
    .run(appRun);

  appRun.$inject = ['routerHelper'];
  /* @ngInject */
  function appRun(routerHelper) {
    routerHelper.configureStates(getStates());
  }

  function getStates() {
    return [
      {
        state: 'welcome',
        config: {
          url: '/welcome',
          templateUrl: 'app/welcome/welcome.html',
          controller: 'WelcomeController',
          controllerAs: 'vm',
          title: 'Welcome',
          settings: {
            nav: 2,
            content: '<i class="fa fa-lock"></i> Welcome'
          }
        }
      }
    ];
  }

Die Directive uiView in der Datei /src/client/app/layout/shell.html gibt an, an welcher Stelle sich die Routing-Zustände befinden (<div ui-view ...></div>). Je nach aktivem Zustand wird an dieser Stelle das entsprechende Zustands-Template mit zugehörigem Controller von Angular dargestellt.

Die Datei shell.html wird innerhalb des body tags von index.html durch die Directive ngInclude eingebunden (<div ng-include="'app/layout/shell.html'"></div>). Durch ngInclude können externe Templates in das aktuelle Template eingebunden werden.

Der Name des neu registrierten Zustands ist welcome, der aktiviert wird, wenn die URL /welcome über einen speziellen Link in der Navigations- (<a ui-sref="welcome"> siehe: /src/client/app/layout/sidebar.html) oder über die Adressleiste des Browsers betreten wird. Anschließend wird das Template welcome.html angezeigt für das der WelcomeController zuständig ist. Durch controllerAs-Syntax kann der Controller innerhalb der View mit vm angesprochen werden.

Fazit Teil 1 und Aussicht Teil 2

Nach Absolvieren der einzelnen Schritte besteht das Projekt nun aus folgenden Elementen:

  • Ein REST-Server mit NoSQL-Datenbank, in die über XHR-Calls, neue Helden eingetragen, verändert, gelöscht oder angezeigt werden können.
  • Das grafische Grundgerüst für eine Web-Applikation mit Navigationsleiste.
  • Routing zwischen einzelnen Views innerhalb der Applikation.
  • Ein Modul für eine Welcome-View.
  • Ein Controller für eine Welcome-View.
  • Ein Route-Zustand für eine Welcome-View.

Der entsprechende Stand der Applikation befindet sich innerhalb des folgenden Repositories.

Im zweiten Teil dieses Tutorials wird die Applikation um weitere Konzepte wie Services, Promises, Two-Way Data Binding, Kommunikation mit dem Server und Event-Handling erweitert. Zudem wird eine neue View hinzugefügt, mit der es möglich ist, über eine grafische Oberfläche Manipulationen an der Datenbank durchzuführen.

Diesen Beitrag teilen

Malte Kreutzfeldt
Software Development
Stets mit offenen Augen für neue Open Source Tools, Frameworks und Best Practice Entwicklungsmethoden lässt er der Erweiterung seines IT-Horizonts schier keine Grenzen.