3. Der Angular-Client JS
3.1. Referenzen zum Angular-Framework JS
Zu Beginn dieses Dokuments wurden zwei Literaturhinweise zum Angular-Framework JS genannt. Wir geben sie hier noch einmal wieder:
- [ref1]: das Buch „Pro AngularJS“ von Adam Freeman, erschienen bei Apress. Es ist ein ausgezeichnetes Buch. Der Quellcode der Beispiele aus diesem Buch ist kostenlos unter URL [http://www.apress.com/downloadable/download/sample/sample_id/1527/] verfügbar;
- [ref2]: die offizielle Dokumentation zu Angular JS [https://docs.angularjs.org/guide];
Angular JS verdient ein eigenes Buch. Das Werk von Adam Freeman umfasst mehr als 600 Seiten, und keine davon ist überflüssig. Wir werden eine Angular-Anwendung beschreiben und dabei auf die Grundlagen dieses Frameworks eingehen. Wir beschränken uns jedoch auf die Erklärungen, die zum Verständnis der vorgeschlagenen Lösung notwendig sind. Angular ist ein äußerst umfangreiches Framework, und es gibt zahlreiche Lösungen, um zum gleichen Ergebnis zu gelangen. Das stellt eine Schwierigkeit dar, denn als Anfänger weiß man nicht, ob man eine Lösung verwendet, die schlechter oder besser ist als eine andere. Dies gilt auch für die hier vorgeschlagene Lösung. Sie könnte anders geschrieben werden, möglicherweise unter Berücksichtigung bewährter Praktiken.
3.2. Architektur des Angular-Clients
Die Architektur des Angular-Clients ähnelt der einer klassischen Webanwendung MVC, weist jedoch einige Unterschiede auf. Eine Spring-Webanwendung MVC hat beispielsweise folgende Architektur:
![]() |
Die Bearbeitung einer Anfrage eines Clients läuft wie folgt ab:
- Anfrage – die angeforderten URL haben die Form http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&.... [Dispatcher Servlet] ist die Spring-Klasse, die eingehende URL verarbeitet. Sie „leitet“ die URL an die Aktion weiter, die sie verarbeiten soll. Diese Aktionen sind Methoden bestimmter Klassen, die als [Contrôleurs] bezeichnet werden. Das „C“ in MVC ist hier die Zeichenkette [Dispatcher Servlet, Contrôleur, Action]. Wenn keine Aktion zur Verarbeitung des eingehenden URL konfiguriert wurde, antwortet das Servlet [Dispatcher Servlet], dass das angeforderte URL nicht gefunden wurde (Fehler 404 NOT FOUND);
- Verarbeitung
- Die ausgewählte Aktion kann die Parameter parami nutzen, die ihr das Servlet [Dispatcher Servlet] übermittelt hat. Diese können aus verschiedenen Quellen stammen:
- des Pfads [/param1/param2/...] des URL,
- der Parameter [p1=v1&p2=v2] aus URL,
- der vom Browser zusammen mit seiner Anfrage gesendeten Parameter;
- Bei der Bearbeitung der Benutzeranfrage benötigt die Aktion möglicherweise die Schichten [metier] und [2b]. Sobald die Anfrage des Clients bearbeitet wurde, kann dies verschiedene Antworten auslösen. Ein klassisches Beispiel ist:
- eine Fehlerseite, wenn die Anfrage nicht korrekt verarbeitet werden konnte
- ansonsten eine Bestätigungsseite
- die Aktion fordert die Anzeige einer bestimmten Ansicht an: [3]. Diese Ansicht zeigt Daten an, die als Modell der Ansicht bezeichnet werden. Das ist das M in MVC. Die Aktion erstellt dieses Modell M [2c] und fordert eine Ansicht V auf, sich anzuzeigen: [3];
- Antwort – die ausgewählte Ansicht V verwendet das von der Aktion erstellte Modell M, um die dynamischen Teile der Antwort HTML zu initialisieren, die sie an den Client senden muss, und sendet diese Antwort anschließend.
Die Architektur unseres Angular-Clients wird analog sein, allerdings mit einer etwas anderen Terminologie. Zunächst einmal sind Angular-Anwendungen in der Regel Single-Page-Webanwendungen (APU) oder Single-Page-Applications (SPA):

- Der Benutzer fordert die Startseite der Anwendung in der Form URL an: http://machine:port/contexte. Der Browser fragt einen Webserver ab, um das angeforderte Dokument zu erhalten. Dabei handelt es sich um eine Seite HTML, die mit CSS gestaltet und durch JavaScript dynamisiert wird;
- anschließend interagiert der Benutzer mit den ihm angezeigten Ansichten. Man kann verschiedene Arten von Interaktionen unterscheiden:
- solche, die keine Interaktion mit der Außenwelt erfordern, zum Beispiel das Ein- und Ausblenden von Elementen der Ansicht. Diese werden durch das eingebettete JavaScript verarbeitet;
- solche, die Daten von einem entfernten Webdienst erfordern. Diese werden über einen AJAX-Aufruf (Asynchronous JavaScript and XML) abgerufen, ein Modell wird erstellt und eine Ansicht angezeigt;
- solche, die eine andere Ansicht als die ursprüngliche erfordern. Diese wird über einen Ajax-Aufruf an den Server angefordert, der die ursprüngliche Seite bereitgestellt hat. Anschließend wiederholt sich der vorherige Vorgang. Die abgerufene Seite wird im Browser zwischengespeichert. Beim nächsten Aufruf wird sie nicht erneut vom entfernten Server HTML angefordert;
Letztendlich führt der Browser nur einen einzigen Aufruf HTTP durch, nämlich den, der die Startseite abruft. Die folgenden Aufrufe HTTP an den Seitenserver HTML oder an entfernte Webdienste werden durch das in den Seiten eingebettete JavaScript ausgeführt.
Wir stellen nun die Architektur der Anwendung innerhalb des Browsers vor. Wir lassen den Server HTML außer Acht, der die Seiten HTML der Anwendung bereitstellt. Zur Vereinfachung können wir davon ausgehen, dass sich alle diese Seiten im Cache des Browsers befinden.
![]() |
Zunächst muss man diese Architektur einordnen:
- Bei [1] befinden wir uns in einem Browser;
- in [2] interagiert ein Benutzer mit den vom Browser angezeigten Ansichten;
- in [3] werden die Daten im Netzwerk abgerufen, häufig von Webdiensten;
Der Benutzer interagiert mit Ansichten: Er füllt Formulare aus und übermittelt sie. Lassen Sie uns diesen Prozess anhand der oben gezeigten Ansicht V1 verdeutlichen. Wir nehmen an, dass es sich um die Startansicht der Anwendung handelt. Sie wurde wie folgt aufgerufen:
- Der Benutzer fordert die Startansicht der Anwendung URL in der Form http://machine:port/contexte an;
- Der Browser hat das zu dieser URL gehörende Dokument angefordert. Er hat die Seite HTML / CSS / JS aus der Ansicht V1 erhalten;
- das in die Seite eingebettete JavaScript übernahm daraufhin die Steuerung und übergab die Kontrolle an den Controller C1 [5];
- dieses hat das Modell M1 [8] [9] der Ansicht V1 erstellt. Die Erstellung dieses Modells erforderte möglicherweise die Nutzung interner Dienste [6] und die Abfrage externer Dienste [7];
Der Benutzer hat nun die Ansicht V1 vor sich. Nehmen wir an, es handelt sich um ein Formular. Er füllt es aus und bestätigt es anschließend:
- In [4] übermittelt der Benutzer das Formular;
- in [5] wird dieses Ereignis von einer der Methoden des Controllers C1 verarbeitet;
Wenn das Ereignis lediglich eine einfache Änderung der Ansicht V1 zur Folge hat (Felder ausblenden/anzeigen), ändert der Controller C1 das Modell M1 der Ansicht V1 und zeigt anschließend die Ansicht V1 erneut an. Dazu benötigt er möglicherweise einen der Dienste der Schicht [services] [6].
Wenn das Ereignis externe Daten erfordert:
- in [6] fordert der Controller C1 die Schicht [DAO] auf, diese zu beschaffen;
- in [7] führt diese einen oder mehrere Aufrufe von AJAX durch, um sie zu beschaffen;
- in [8] und [9] wird das Modell M1 geändert und die Ansicht V1 angezeigt;
Wenn das Ereignis in den beiden vorangegangenen Fällen zu einem Wechsel der Ansicht führt, fordert der Controller C1 – anstatt die Ansicht V1 anzuzeigen – eine neue Ansicht URL [10] an. Es handelt sich um eine browserinterne URL. Diese führt nicht sofort zu einem Aufruf von HTTP an den Seitenserver HTML. Diese Änderung von URL wird von einem Router verarbeitet, der so konfiguriert ist, dass jedem internen URL eine Ansicht V und deren Controller C zugeordnet sind. Der Router lässt dann die neue Ansicht Vn anzeigen. Vor der Anzeige übernimmt der zugehörige Controller Cn die Steuerung, erstellt das Modell Mn und lässt anschließend die Ansicht Vn [11] anzeigen. Wenn die Seite HTML der Ansicht Vn nicht im Cache des Browsers vorhanden war, wird sie vom Seitenserver HTML angefordert.
Die Schicht [Présentation] dieser Architektur ähnelt der Architektur JSF (Java Server Faces):
- Die Ansicht V entspricht der Facelet-Ansicht von JSF;
- der Controller C entspricht dem Bean JSF, einer Java-Klasse, die sowohl das Modell M der Ansicht V als auch deren Ereignisbehandler enthält;
Die Schicht [Services] unterscheidet sich von den bekannten Schichten [Services]. In der serverseitigen Webentwicklung findet man meist die folgende Schichtenarchitektur:
![]() |
In der obigen Darstellung kommuniziert die Schicht [web] mit der Schicht [DAO] ausschließlich über die Schicht [métier]. Nichts würde uns daran hindern, in die Schicht [web] eine Referenz auf die Schicht [DAO] einzufügen, die diese Kommunikation ermöglichen würde. Aber wir verzichten darauf.
Mit Angular verzichten wir nicht darauf. Die Architektur sieht dann wie folgt aus:
![]() |
- In [1] kann die Schicht [présentation] direkt mit jedem beliebigen Dienst kommunizieren;
- in [2] kennen sich die Dienste untereinander. Ein Dienst kann einen oder mehrere andere Dienste nutzen.
3.3. Die Ansichten des Angular-Clients
Die Ansichten des Angular-Clients wurden bereits in Abschnitt 1.3.3 vorgestellt. Um das Lesen dieses neuen Kapitels zu erleichtern, geben wir sie hier noch einmal wieder. Die erste Ansicht lautet wie folgt:
![]() |
- in [6], die Startseite der Anwendung. Es handelt sich um eine Anwendung zur Terminvereinbarung für Ärzte;
- in [7] ein Kontrollkästchen, mit dem man den Modus [debug] aktivieren oder deaktivieren kann. Letzterer zeichnet sich durch das Vorhandensein des Rahmens [8] aus, der die Vorlage der aktuellen Ansicht anzeigt;
- in [9] eine künstliche Wartezeit in Millisekunden. Der Standardwert beträgt 0 (keine Wartezeit). Wenn N der Wert dieser Wartezeit ist, wird jede Aktion des Benutzers nach einer Wartezeit von N Millisekunden ausgeführt. So lässt sich die von der Anwendung implementierte Wartezeitverwaltung beobachten;
- in [10] die URL des Spring-4-Servers. Folgt man dem Vorhergehenden, ist dies [http://localhost:8080];
- in [11] und [12] die Benutzer-ID und das Passwort desjenigen, der die Anwendung nutzen möchte. Es gibt zwei Benutzer: admin/admin (Login/Passwort) mit einer Rolle (ADMIN) und user/user mit einer Rolle (USER). Nur die Rolle ADMIN ist berechtigt, die Anwendung zu nutzen. Die Rolle USER dient lediglich dazu, zu veranschaulichen, welche Antwort der Server in diesem Anwendungsfall zurückgibt;
- in [13] die Schaltfläche, über die man sich beim Server anmelden kann;
- in [14] die Sprache der Anwendung. Es gibt zwei: standardmäßig Französisch und Englisch.
![]() |
- in [1] wird die Verbindung hergestellt;
![]() |
- Sobald man angemeldet ist, kann man den Arzt auswählen, bei dem man einen Termin vereinbaren möchte ([2]), sowie den Tag des Termins ([3]);
- man fordert unter [4] den Terminkalender des ausgewählten Arztes für den gewählten Tag an;
![]() |
- Sobald der Terminkalender des Arztes angezeigt wird, kann man einen Termin reservieren [5];
![]() |
- In [6] wählt man den Patienten für den Termin aus und bestätigt diese Auswahl in [7];
![]() |
Sobald der Termin bestätigt ist, kehrt man automatisch zum Kalender zurück, in dem der neue Termin nun eingetragen ist. Dieser Termin kann später über [7] gelöscht werden.
Die wichtigsten Funktionen wurden beschrieben. Sie sind einfach. Die nicht beschriebenen Funktionen sind Navigationsfunktionen, um zu einer vorherigen Ansicht zurückzukehren. Schließen wir mit der Sprachverwaltung ab:
![]() |
- In [1] wechselt man von Französisch zu Englisch;
2

- zu [2] wechselt die Ansicht ins Englische, einschließlich des Kalenders;
3.4. Konfiguration des Angular-Projekts
Wir werden unseren Angular-Client schrittweise aufbauen. Wir verwenden WebStorm.
Erstellen wir einen leeren Ordner [rdvmedecins-angular-v1] und öffnen wir ihn anschließend mit WebStorm:
![]() |
- In [1] öffnen wir einen Ordner;
- in [2] wählen wir den Ordner aus, den wir erstellt haben;
- In [3] erhalten wir ein leeres WebStorm-Projekt;
![]() |
- in [4] erfolgt die Projektkonfiguration über die Option [File / Settings];
- In den Dateien „[5]“ und „[6]“ wird die Eigenschaft „[Spelling]“ konfiguriert, die die Rechtschreibprüfung steuert. Standardmäßig ist diese aktiviert. Da die heruntergeladene Software auf Englisch ist, werden unsere französischen Kommentare zu den Programmen als mögliche Rechtschreibfehler markiert. Daher deaktivieren wir diese Rechtschreibprüfung [7];
![]() |
- in [8] erstellen wir eine neue Datei;
- in [9] wählen wir die Erstellung der Datei [package.json], die die Anwendung mit einer Syntax JSON beschreibt;
- In [10] wird die generierte Datei so bearbeitet, wie in [11] gezeigt;
- in [12] wird diese Datei sowohl in [package.json] als auch in [bower.json] gespeichert;
![]() |
- In [13] wird das Projekt neu konfiguriert;
![]() |
- in [14] konfigurieren wir die Eigenschaft [Javascript / Bower], die es uns ermöglicht, die benötigten JavaScript-Bibliotheken anzugeben;
- in [15] geben wir die soeben erstellte Datei [bower.json] an;
![]() |
- Fügen wir in [16] eine JavaScript-Bibliothek hinzu;
- In [17] werden alle herunterladbaren JavaScript-Bibliotheken angezeigt;
- In [18] können wir einen Filter anwenden, um die Liste [17] einzugrenzen. Hier geben wir an, dass wir die Bibliothek [Angular JS] möchten;
- In [19] werden die Eigenschaften der Bibliothek angezeigt. Hier sehen wir, dass die Version 1.2.18 von Angular heruntergeladen wird;
- in [20] wird sie heruntergeladen;
![]() |
- in [21] sieht man, dass sie heruntergeladen wurde;
- in [22] sieht man die heruntergeladene Version. Es handelt sich also tatsächlich um die Version 1.2.19;
- in [23] sieht man die aktuellste verfügbare Version;
![]() |
- Bei [24] werden nach dem gleichen Verfahren wie zuvor die folgenden Bibliotheken heruntergeladen:
um die Zeichenfolge „user:password“ in Base64 zu kodieren; | ||
zur Internationalisierung des Kalenders | ||
um die anwendungsinternen URL an den richtigen Controller und die richtige Ansicht weiterzuleiten; | ||
ermöglicht die Internationalisierung der Ansichten. Es handelt sich um ein von Angular unabhängiges Projekt. Hier werden zwei Sprachen verwendet: Französisch und Englisch; | ||
stellt Bootstrap-kompatible visuelle Komponenten bereit. Hier wird dessen Kalender verwendet; | ||
das Bootstrap-Framework CSS. Es wird zum Erstellen der Ansichten verwendet; | ||
stellt eine visuelle Komponente vom Typ „Tabelle“ bereit. Sie ist insofern „responsive“, als sie sich an die Bildschirmgröße anpassen kann; | ||
stellt eine Komponente vom Typ „Dropdown-Liste“ bereit; |
![]() |
- In [25] wurden die heruntergeladenen Bibliotheken im Ordner [bower_components] installiert;
- In [26] ist zu sehen, dass die Bibliothek JQuery heruntergeladen wurde. Das liegt daran, dass Bootstrap sie verwendet. Das System zur Installation der JavaScript-Abhängigkeiten eines Projekts funktioniert ähnlich wie bei Maven in der Java-Welt: Wenn eine heruntergeladene Bibliothek selbst Abhängigkeiten hat, werden diese automatisch heruntergeladen;
Die Datei [bower.json] hat sich geändert:
Alle heruntergeladenen Abhängigkeiten wurden in die Datei eingetragen.
3.5. Die Startseite des Angular-Clients
Wir erstellen eine erste Version der Startseite des Angular-Clients:
![]() |
- in den Dateien [1] und [2], wir erstellen eine Datei HTML mit den Namen [app-01], [3] und [4];
Die Datei [app-01.html] wird für eine Weile unsere Hauptseite sein. Dort werden wir den Import der Dateien CSS und JS konfigurieren, die die Anwendung benötigt:
<!DOCTYPE html>
<html>
<head>
<title>RdvMedecins</title>
<!-- META -->
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Angular client for RdvMedecins">
<meta name="author" content="Serge Tahé">
<!-- das CSS -->
<link href="bower_components/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" />
<link href="bower_components/bootstrap/dist/css/bootstrap-theme.min.css" rel="stylesheet"/>
<link href="bower_components/bootstrap-select/bootstrap-select.min.css" rel="stylesheet"/>
<link href="bower_components/footable/css/footable.core.min.css" rel="stylesheet"/>
</head>
<body>
<div class="container">
<h1>Rdvmedecins - v1</h1>
</div>
<!-- Bootstrap-Kern JavaScript ================================================== -->
<script type="text/javascript" src="bower_components/jquery/dist/jquery.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap-select/bootstrap-select.min.js"></script>
<script type="text/javascript" src="bower_components/footable/dist/footable.min.js"></script>
<!-- AngularJS -->
<script type="text/javascript" src="bower_components/angular/angular.min.js"></script>
<script type="text/javascript" src="bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.min.js"></script>
<script type="text/javascript" src="bower_components/angular-route/angular-route.min.js"></script>
<script type="text/javascript" src="bower_components/angular-translate/angular-translate.min.js"></script>
<script type="text/javascript" src="bower_components/angular-base64/angular-base64.min.js"></script>
</body>
</html>
- Zeilen 11–12: die Dateien „CSS“ für Bootstrap;
- Zeile 13: die Datei CSS für die Komponente [boostrap-select];
- Zeile 14: die Datei CSS für die Komponente [footable];
- Zeilen 21–24: die Dateien JS der Bootstrap-Komponenten;
- Zeile 21: Die Bootstrap-Komponenten basieren auf JQuery;
- Zeile 22: die Datei JS von Bootstrap;
- Zeile 23: die Datei JS für die Komponente [boostrap-select];
- Zeile 24: die Datei JS für die Komponente [footable];
- Zeilen 26–30: die Dateien JS von Angular und den damit verbundenen Projekten;
- Zeile 26: die Datei JS von Angular. Sie muss nach JQuery geladen werden, falls diese Bibliothek verwendet wird;
- Zeile 27: die Datei JS des Projekts [angular-ui-bootstrap];
- Zeile 28: die Datei JS des Routers [angular-route];
- Zeile 29: die Datei JS aus dem Modul zur Internationalisierung von Angular-Anwendungen;
- Zeile 30: die Datei JS des Moduls [angular-base64];
Die Gültigkeit der Datei [app-01.html] kann überprüft werden:
![]() |
- in [1] wird die Codeüberprüfung angefordert;
- in [2] das Ergebnis, wenn alles in Ordnung ist;
Diese systematische Überprüfung des Codes vor dessen Ausführung wird empfohlen. In diesem Fall ermöglicht diese Überprüfung, alle Verweisfehler in den Dateien CSS und JS zu erkennen. Ist ein Pfad falsch, wird dies vom Code-Inspektor gemeldet.
- In [3] kann die Seite über einen Debugger in einen Browser geladen werden. Im Browser erhält man folgendes Ergebnis:
![]() |
- In [4] wurde die Seite [app-01.html] von einem internen WebStorm-Server bereitgestellt, der hier auf Port 63342 läuft;
- bei [5] die Debugger-Konsole. Wären Fehler aufgetreten, wären sie hier angezeigt worden. Dort werden auch die Bildschirmausgaben angezeigt, die durch die JavaScript-Anweisung [console.log(expression)] erzeugt werden. Wir werden diese Möglichkeit ausgiebig nutzen;
Der Debug-Modus ermöglicht es, die Seite in WebStorm zu ändern und die Ergebnisse dieser Änderungen im Browser zu sehen, ohne die Seite neu laden zu müssen. Wenn wir also die folgende Zeile 3 hinzufügen:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<h2>Version 1</h2>
</div>
und kehren dann zum Browser zurück, stellen wir fest, dass sich die Seite geändert hat:
![]() |
3.6. Einführung in Bootstrap
Wir werden nun einige der in der Anwendung verwendeten Funktionen von Bootstrap veranschaulichen. Ich verfüge nur über begrenzte Kenntnisse dieses Frameworks, die ich mir durch das Kopieren und Einfügen von im Internet gefundenem Code angeeignet habe. Ich werde die Rolle der Klassen CSS erläutern, die ich, glaube ich, verstehe. Auf die anderen werde ich nicht näher eingehen.
3.6.1. Beispiel 1
In Angular sind Vorgänge, die Informationen von externen Quellen abrufen, asynchron. Das bedeutet, dass der Vorgang gestartet wird und die Ansicht sofort zurückkehrt, mit der der Benutzer weiterhin interagieren kann. Die Anwendung wird durch ein Ereignis über den Abschluss des Vorgangs informiert. Dieses Ereignis wird von einer Funktion JS verarbeitet, die daraufhin die aktuelle Ansicht ergänzen oder ändern kann. Wenn der Vorgang voraussichtlich lange dauern wird, ist es sinnvoll, dem Benutzer die Möglichkeit zu geben, ihn abzubrechen. Wir werden ihm diese Möglichkeit systematisch anbieten. Dazu verwenden wir ein Bootstrap-Banner:

Um dieses Ergebnis zu erzielen, duplizieren wir [app-01.html] in [app-02.html] und ändern die folgenden Zeilen:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<div class="alert alert-warning">
<h1>Opération en cours. Veuillez patienter...
<button class="btn btn-primary pull-right">Annuler</button>
<img src="assets/images/waiting.gif" alt=""/>
</h1>
</div>
</div>
- Zeile 1: Die Klasse CSS [container] definiert einen Anzeigebereich innerhalb des Browsers;
- Zeile 3: Die Klasse CSS [alert] zeigt einen farbigen Bereich an. Die Klasse [alert-warning] verwendet eine vordefinierte Farbe;
- Zeile 5: Die Klasse [btn] gestaltet eine Schaltfläche. Die Klasse [btn-primary] weist ihr eine bestimmte Farbe zu. Die Klasse [pull-right] positioniert sie rechts im Warnbanner;
- Zeile 6: ein animiertes Ladebild;
3.6.2. Beispiel 2
Die verschiedenen Ansichten der Anwendung haben einen gemeinsamen Titel:

Um dieses Ergebnis zu erzielen, duplizieren wir [app-01.html] in [app-03.html] und ändern die folgenden Zeilen:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- Bootstrap Jumbotron -->
<div class="jumbotron">
<div class="row">
<div class="col-md-2">
<img src="assets/images/caduceus.jpg" alt="RvMedecins"/>
</div>
<div class="col-md-10">
<h1>Les Médecins associés</h1>
</div>
</div>
</div>
</div>
- Der farbige Bereich wird durch die Klasse [jumbotron] in Zeile 4 erzeugt;
- Zeile 5: Die Klasse [row] definiert eine Zeile mit 12 Spalten;
- Zeile 6: Die Klasse [col-md-2] definiert einen zweispaltigen Bereich in der Zeile;
- Zeile 7: In diese beiden Spalten wird ein Bild eingefügt;
- Zeilen 9–11: In die übrigen 10 Spalten wird der Text eingefügt;
3.6.3. Beispiel 3
Die Ansichten verfügen über eine obere Steuerleiste. Dort befinden sich Steuerungsoptionen, Links oder Schaltflächen. Außerdem befinden sich dort Formularelemente. Zum Beispiel:
![]() |
Um dieses Ergebnis zu erzielen, duplizieren wir [app-01.html] in [app-04.html] und ändern die folgenden Zeilen:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="navbar-collapse collapse">
<form class="navbar-form navbar-right">
<!-- Debug-Modus -->
<label style="width: 100px">
<input type="checkbox">
<span style="color: white">Debug</span>
</label>
<!-- Anmeldeformular -->
<div class="form-group">
<input type="text" class="form-control" placeholder="Temps d'attente"
style="width: 150px"/>
<input type="text" class="form-control" placeholder="URL du service web"
style="width: 200px"/>
<input type="text" class="form-control" placeholder="Login"
style="width: 100px"/>
<input type="password" class="form-control" placeholder="Mot de passe"
style="width: 100px"/>
</div>
<button class="btn btn-success">
Connexion
</button>
</form>
</div>
<button class="btn btn-success">
Connexion
</button>
</form>
</div>
</div>
</div>
</div>
- Zeile 4: Die Klasse [navbar] legt das Design der Navigationsleiste fest. Die Klasse [navbar-inverse] weist ihr einen schwarzen Hintergrund zu. Die Klasse [navbar-fixed-top] sorgt dafür, dass die Navigationsleiste beim Scrollen der vom Browser angezeigten Seite oben auf dem Bildschirm bleibt;
- Zeilen 6–14: Definieren den Bereich [1]. Das ist typischerweise eine Reihe von Klassen, die ich nicht verstehe. Ich verwende die Komponente so, wie sie ist;
- Zeile 15: Definiert einen „responsiven“ Bereich der Steuerleiste. Auf einem Smartphone verschwindet dieser Bereich in einem Menübereich;
- Zeile 16: Die Klasse [navbar-form] gestaltet ein Formular in der Befehlsleiste. Die Klasse [navbar-right] verschiebt es rechts davon;
- Zeilen 23–32: Die vier Eingabefelder des Formulars aus Zeile 17 [3]. Sie befinden sich innerhalb einer Klasse [form-group], die die Elemente eines Formulars gestaltet, und jedes von ihnen hat die Klasse [form-control];
- Zeile 33: Die bereits bekannte Klasse [btn], ergänzt um die Klasse [btn-success], die ihr die grüne Farbe verleiht;
3.6.4. Beispiel 4
Über die Steuerleiste lässt sich die Sprache mithilfe einer Dropdown-Liste ändern:

Um dieses Ergebnis zu erzielen, duplizieren wir [app-01.html] in [app-05.html] und fügen der Steuerleiste die folgenden Zeilen hinzu:
<button class="btn btn-success">
Connexion
</button>
<!-- Sprachen -->
<div class="btn-group">
<button type="button" class="btn btn-danger">
Langues
</button>
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu" role="menu">
<li>
<a href="">Français</a>
</li>
<li>
<a href="">English</a>
</li>
</ul>
</div>
</form>
Die hinzugefügten Zeilen sind die Zeilen 4–21.
- Zeile 5: Die Klasse [btn-group] legt das Aussehen einer Gruppe von Schaltflächen fest. Es gibt zwei davon in den Zeilen 6 und 9;
- Zeilen 6–8: Die erste Schaltfläche definiert die Beschriftung der Dropdown-Liste. Die Klasse [btn-danger] weist ihr die Farbe Rot zu;
- Zeilen 9–12: Die zweite Schaltfläche ist die der Dropdown-Liste. Sie ist an die erste angefügt, wodurch der Eindruck einer einzigen Komponente entsteht;
- Zeile 10: Zeigt den Abwärtspfeil an, der darauf hinweist, dass es sich bei der Schaltfläche um eine Dropdown-Liste handelt;
- Zeile 11: für „Screenreader“;
- Zeilen 13–20: Die Elemente der Dropdown-Liste sind Elemente einer ungeordneten Liste;
3.6.5. Beispiel 5
Um ein Formular zu übermitteln oder zu navigieren, stehen dem Benutzer in der Steuerleiste Optionen oder Schaltflächen wie unten dargestellt zur Verfügung:
![]() |
Menüoptionen wurden in [1] eingerichtet. Um dieses Ergebnis zu erzielen, duplizieren wir [app-01.html] in [app-06.html] und fügen die folgenden Zeilen hinzu:
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
...
</div>
<!-- Menüoptionen -->
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li class="active">
<a href="">
<span>Home</span>
</a>
</li>
<li class="active">
<a href="">
<span>Agenda</span>
</a>
</li>
<li class="active">
<a href="">
<span>Valider</span>
</a>
</li>
<li class="active">
<a href="">
<span>Annuler</span>
</a>
</li>
</ul>
<!-- Schaltflächen rechts -->
<form class="navbar-form navbar-right" role="form">
...
</form>
</div>
</div>
</div>
</div>
- Die Menüoptionen werden durch die Zeilen 8–29 erzeugt. Auch hier handelt es sich um Elemente einer <ul>-Liste. Die Klasse [active] sorgt dafür, dass der Text hervorgehoben wird, was anzeigt, dass die Option anklickbar ist.
3.6.6. Beispiel 6
Wir stellen die Ärzte und Kunden in Dropdown-Listen wie folgt dar:
![]() |
Die verwendete Dropdown-Liste ist keine native Bootstrap-Komponente. Es handelt sich um die Komponente [bootstrap-select] (http://silviomoreto.github.io/bootstrap-select/). Um dieses Ergebnis zu erzielen, duplizieren wir [app-01.html] in [app-07.html] und fügen die folgenden Zeilen hinzu:
<!DOCTYPE html>
<html>
<head>
...
<link href="bower_components/bootstrap-select/bootstrap-select.min.css" rel="stylesheet"/>
</head>
<body>
<div class="container">
<h1>Rdvmedecins - v1</h1>
<h2><label for="medecins">Médecins</label></h2>
<select id="medecins" data-style="btn btn-primary" class="selectpicker">
<option value="1">Mme Marie PELISSIER</option>
<option value="1">Mr Jacques BROMARD</option>
<option value="1">Mr Philippe JANDOT</option>
<option value="1">Mme Justine JACQUEMOT</option>
</select>
</div>
<!-- Bootstrap-Kern JavaScript ================================================== -->
...
<script type="text/javascript" src="bower_components/bootstrap-select/bootstrap-select.min.js"></script>
<!-- Lokales Skript -->
<script>
$('.selectpicker').selectpicker();
</script>
</body>
</html>
- Zeile 5: Das Stylesheet von [bootstrap-select] muss importiert werden;
- Zeile 13: Das Attribut [data-style] wird von [bootstrap-select] verwendet. Es dient dazu, der Dropdown-Liste ein Styling zu geben. Hier wird ihr die Form einer blauen Schaltfläche [btn-primary] zugewiesen;
- Zeile 13: Das Attribut [class] wird in Zeile 23 verwendet. Kann beliebig sein;
- Zeilen 14–17: Die Elemente der Dropdown-Liste. Hier finden sich die klassischen Tags HTML;
- Zeile 22: Das JS muss aus [bootstrap-select] importiert werden;
- Zeilen 24–26: Ein Skript JS, das nach dem Laden der Seite ausgeführt wird;
- Zeile 25: eine Anweisung JQuery. Die Methode [selectpicker] (selectpicker()) wird auf alle Elemente mit der Klasse [selectpicker] ($('.selectpicker')) angewendet. Es gibt nur eines, das select-Tag in Zeile 13. Die Methode [selectpicker] stammt aus der Datei JS, auf die in Zeile 22 verwiesen wird;
3.6.7. Beispiel 7
Um den Terminkalender eines Arztes anzuzeigen, verwenden wir eine „responsive“ Tabelle aus der Bibliothek JS [footable]:
![]() |
- in [1]: die Tabelle mit normaler Darstellung;
- in [2]: die Tabelle, wenn die Größe des Browserfensters verkleinert wird. Die Spalte [Action] wird automatisch in die nächste Zeile verschoben. Dies wird als „responsive“ oder einfach als anpassungsfähige Komponente bezeichnet.
Wir duplizieren [app-01.html] in [app-08.html] und fügen die folgenden Zeilen hinzu:
...
<link href="bower_components/footable/css/footable.core.min.css" rel="stylesheet"/>
<link href="assets/css/rdvmedecins.css" rel="stylesheet"/>
...
<div class="container">
<h1>Rdvmedecins - v1</h1>
<div class="row alert alert-warning">
<div class="col-md-6">
<table id="creneaux" class="table">
<thead>
<tr>
<th data-toggle="true">
<span>Créneau horaire</span>
</th>
<th>
<span>Client</span>
</th>
<th data-hide="phone">
<span>Action</span>
</th>
</thead>
<tbody>
<tr>
<td>
<span class='status-metro status-active'>
9h00-9h20
</span>
</td>
<td>
<span></span>
</td>
<td>
<a href="" class="status-metro status-active">
Réserver
</a>
</td>
</tr>
<tr>
<td>
<span class='status-metro status-suspended'>
9h20-9h40
</span>
</td>
<td>
<span>Mme Paule MARTIN</span>
</td>
<td>
<a href="" class="status-metro status-suspended">
Supprimer
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
...
<script src="bower_components/footable/dist/footable.min.js" type="text/javascript"></script>
- Die Zeilen 2 und 60 sind bereits in [app-01.html] vorhanden. Es handelt sich um die Dateien CSS und JS, die von der Bibliothek [footable] bereitgestellt werden;
- Zeile 3 verweist auf die folgende Datei CSS:
@CHARSET "UTF-8";
#th-Spalten {
text-align: center;
}
#td-Spalten {
text-align: center;
font-weight: bold;
}
.status-metro {
display: inline-block;
padding: 2px 5px;
color:#fff;
}
.status-metro.status-active {
background: #43c83c;
}
.status-metro.status-suspended {
background: #fa3031;
}
Die Stile [status-*] stammen aus einem Anwendungsbeispiel für die Tabelle [footable], das auf der Website der Bibliothek zu finden ist.
- Zeile 8: Fügt die Tabelle in eine Zeile [row] und einen farbigen Rahmen [alert alert-warning] ein;
- Zeile 9: Die Tabelle nimmt 6 Spalten ein ([col-md-6]);
- Zeile 10: Die Tabelle HTML wird mit Bootstrap formatiert [class='table'];
- Zeile 13: Das Attribut [data-toggle] gibt die Spalte an, in der sich das Symbol [+/-] befindet, mit dem die Zeile ein- und ausgeklappt wird;
- Zeile 19: Das Attribut [data-hide='phone'] gibt an, dass die Spalte ausgeblendet werden soll, wenn der Bildschirm die Größe eines Smartphone-Bildschirms hat. Es kann auch der Wert „tablet“ verwendet werden;
3.6.8. Beispiel 8
Um dem Benutzer zu helfen, erstellen wir Tooltips um die wichtigsten Komponenten der Ansichten:
![]() |
Um dieses Ergebnis zu erzielen, duplizieren wir [app-01.html] in [app-09.html] und fügen die folgenden Zeilen hinzu:
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
...
</head>
<body>
<div class="container">
<h1>Rdvmedecins - v1</h1>
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<!-- Menüoptionen -->
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li class="active">
<a href="">
<span tooltip="Retourne à la page d'accueil" tooltip-placement="bottom">Home</span>
</a>
</li>
<li class="active">
<a href="">
<span tooltip="Affiche l'agenda" tooltip-placement="top">Agenda</span>
</a>
</li>
<li class="active">
<a href="">
<span tooltip="Valide le rendez-vous" tooltip-placement="right">Valider</span>
</a>
</li>
<li class="active">
<a href="">
<span tooltip="Annule l'opération en cours" tooltip-placement="left">Annuler</span>
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
<!-- Bootstrap-Kern JavaScript ================================================== -->
<...
<script type="text/javascript" src="bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.min.js"></script>
<!-- Lokales Skript -->
<script>
// --------------------- Angular-Modul
angular.module("rdvmedecins", ['ui.bootstrap']);
</script>
</body>
</html>
Die Hilfe-Sprechblasen werden von der Bibliothek „[angular-ui-bootstrap]“ bereitgestellt, die ihrerseits auf der Bibliothek „[angular]“ basiert. In Zeile 50 wird die Bibliothek „[angular-ui-bootstrap]“ importiert. Um die Komponenten der Bibliothek [angular-ui-bootstrap] zu implementieren, müssen wir ein Angular-Modul erstellen. Dies geschieht in den Zeilen 52–55. Diese Zeilen definieren ein Angular-Modul mit dem Namen [rdvmedecins] (1. Parameter). Ein Angular-Modul kann andere Angular-Module verwenden. Dies werden als Modulabhängigkeiten bezeichnet. Sie werden in einem Array als zweiter Parameter der Funktion [angular.module] übergeben. Hier wird das Modul mit dem Namen [ui.bootstrap] von der Bibliothek [angular-ui-bootstrap] bereitgestellt. Dieses Modul liefert uns die Hilfe-Tooltips.
Zeile 54 definiert ein Angular-Modul. Standardmäßig hat dies keine Auswirkungen auf die Seite. Wir geben an, dass die Seite von Angular verwaltet werden soll, indem wir sie einem Angular-Modul zuordnen. Dies geschieht in Zeile 2. Das Attribut [ng-app='rdvmedecins'] ordnet die Seite dem in Zeile 54 erstellten Modul zu. Die Seite wird nun von Angular analysiert. Die Attribute [tooltip] werden vom Modul [ui.bootstrap] erkannt und verarbeitet.
Die Syntax der Hilfe-Blase lautet wie folgt:
<span tooltip="Retourne à la page d'accueil" tooltip-placement="bottom">Home</span>
Im obigen Beispiel wird dem Text „[Home]“ ein Tooltip hinzugefügt:
- [tooltip]: definiert den Text des Tooltips;
- [tooltip-placement]: legt die Position fest (bottom, top, left, right);
Angular JS ermöglicht es, neue Tags oder Attribute zu den bereits in der Sprache HTML vorhandenen hinzuzufügen. Diese Erweiterung der Sprache HTML erfolgt mithilfe von Angular-Direktiven. Hier sind die Attribute [tooltip] und [tooltip-placement] Attribute, die von [angular-ui-bootstrap] erstellt wurden.
3.6.9. Beispiel 9
Um dem Benutzer die Auswahl des Tages für einen Termin zu erleichtern, stellen wir ihm einen Kalender zur Verfügung:

Wie bei den Hilfe-Tooltips wird dieser Kalender von der Bibliothek [angular-ui-bootstrap] bereitgestellt. Um dieses Ergebnis zu erzielen, duplizieren wir [app-01.html] in [app-10.html] und fügen die folgenden Zeilen hinzu:
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
...
<body>
<div class="container">
<h1>Rdvmedecins - v1</h1>
<div>
<pre>Date <em>{{jour | date:'fullDate'}}</em></pre>
<div class="row">
<div class="col-md-2">
<h4>Calendrier</h4>
<div style="display:inline-block; min-height:290px;">
<datepicker ng-model="jour" show-weeks="true" class="well"></datepicker>
</div>
</div>
</div>
</div>
</div>
</div>
...
<!-- Lokales Skript -->
<script>
// --------------------- Angular-Modul
angular.module("rdvmedecins", ['ui.bootstrap'])
</script>
</body>
</html>
Wie zuvor ist die Seite einem Angular-Modul zugeordnet (Zeilen 2 und 28). Der Kalender wird durch das <datepicker>-Tag in Zeile 16 definiert, das von der Bibliothek [angular-ui-bootstrap] bereitgestellt wird:
- [show-weeks='true']: zur Anzeige der Wochennummern;
- [class='well']: um den Kalender mit einem grauen Bereich mit abgerundeten Ecken zu umgeben;
- [ng-model='jour']: Die Attribute [ng-*] sind Angular-Attribute. Das Attribut [ng-model] bezeichnet eine Datenangabe, die in das Modell der Ansicht eingefügt wird. Wenn der Benutzer auf ein Datum klickt, wird dieses in die Variable [jour] des Modells gesetzt. Diese Variable wird in Zeile 10 verwendet. Die Syntax {{Ausdruck}} ermöglicht die Auswertung eines Ausdrucks, der aus Elementen des Modells besteht. Hier zeigt {{Tag}} den Wert der Variablen [jour] aus dem Modell an. Ein wesentliches Merkmal von Angular ist, dass die Ansicht Änderungen an der Variablen [jour] automatisch nachverfolgt. Wenn der Benutzer also die Daten ändert, werden diese Änderungen sofort in Zeile 10 angezeigt. Im Allgemeinen funktioniert das wie folgt:
- Eine Ansicht V ist einem Modell M zugeordnet;
- Angular überwacht das Modell M und aktualisiert die Ansicht V automatisch, sobald sich das Modell M ändert;
Die Syntax {{Tag|Datum}} wird als Filter bezeichnet. Es wird nicht der Wert von [jour] angezeigt, sondern der Wert von [jour], gefiltert durch einen Filter namens [date]. Dieser Filter ist in Angular vordefiniert. Er dient zur Formatierung von Datumsangaben. Er akzeptiert Parameter, die das gewünschte Format festlegen. So gibt der Ausdruck {{Tag | date:'fullDate'}} an, dass das vollständige Datumsformat gewünscht ist, in diesem Fall [Friday, June 20, 2014], da der Kalender standardmäßig auf Englisch eingestellt ist. Wir werden uns demnächst mit seiner Internationalisierung befassen.
3.6.10. Fazit
Wir haben die Elemente des CSS-Bootstrap-Frameworks vorgestellt, die wir verwenden werden. Es handelte sich um passive Komponenten: Ihre Ereignisse wurden nicht verarbeitet. Ein Klick auf die Schaltflächen oder Links hatte also keine Auswirkung. Diese Ereignisse werden in JavaScript abgewickelt. Es ist möglich, diese Sprache ohne die Hilfe von Frameworks zu verwenden, aber wie bereits auf der serverseitigen Seite der Fall war, sind auf der clientseitigen Seite bestimmte Frameworks unverzichtbar. Dies gilt für das Angular-Framework JS, das eine neue Herangehensweise an die Entwicklung von JavaScript-Anwendungen mit sich bringt, die in einem Browser ausgeführt werden. Wir stellen es nun vor.
3.7. Einführung in Angular JS
Wir werden nun einige der in der Anwendung verwendeten Funktionen des Angular-Frameworks JS veranschaulichen. Auf einige davon sind wir bereits gestoßen:
- Eine Seite HTML wird von Angular JS gesteuert, wenn man ihr ein Modul zuordnet:
<html ng-app="rdvmedecins">
- Mit Angular lassen sich über Direktiven neue Tags und Attribute HTML erstellen:
- Mit Angular lassen sich Filter erstellen:
- Eine Ansicht V zeigt ein Modell M an. Angular überwacht das Modell M und aktualisiert die Ansicht V automatisch, sobald sich das Modell M ändert. Der Wert einer Variablen des Modells M wird in der Ansicht V wie folgt angezeigt:
Wir werden zunächst die Implementierung des Entwurfsmusters „Model-View-Controller“ in Angular näher betrachten. Erinnern wir uns an die Zusammenhänge zwischen diesen Komponenten aus architektonischer Sicht:
![]() |
- Die Ansicht V1 zeigt das Modell M1 an, das vom Controller C1 erstellt wurde. Letzterer enthält nicht nur das Modell M1, sondern auch die Ereignisbehandler der Ansicht V1. Wir befinden uns im Zyklus 5, 8, 9:
- [5]: In der Ansicht V1 tritt ein Ereignis auf. Es wird vom Controller C1 verarbeitet;
- dieser führt seine Aufgabe [6-7] aus und erstellt anschließend die Vorlage M1 [8];
- [9]: Die Ansicht V1 zeigt das neue Modell M1 an. Wie bereits erwähnt, erfolgt dieser letzte Schritt automatisch. Es gibt keinen expliziten Push wie in anderen Frameworks (C1 schiebt das Modell M1 in V1) oder einen expliziten Pull (die Ansicht V1 ruft das Modell M1 aus C1 ab). Es findet ein impliziter Push statt, den der Entwickler nicht sieht;
- dann setzt sich der Zyklus 5, 8, 9 fort;
3.7.1. Beispiel 1: Das Angular-Template MVC
Wir greifen das Beispiel des Kalenders wieder auf. Wir haben die Direktive gesehen, die ihn generiert:
<datepicker ng-model="jour" show-weeks="true" class="well"></datepicker>
Diese Direktive unterstützt neben den oben vorgestellten Attributen noch weitere, darunter das Attribut [min-date], das das früheste Datum festlegt, das im Kalender ausgewählt werden kann. Das wird uns nützlich sein. Wenn der Benutzer ein Termin-Datum auswählt, muss dieses mindestens dem aktuellen Datum entsprechen. Wir schreiben also:
<datepicker ng-model="jour" ... min-date="dateMin"></datepicker>
wobei [dateMin] eine Variable der Seitenvorlage ist, deren Wert dem aktuellen Datum entspricht. Daraus ergibt sich folgende Seite:
![]() |
- Bei [1] ist heute der 19. Juni 2014. Der Cursor zeigt an, dass man den 19. Juni auswählen kann;
- in [2] zeigt der Cursor an, dass der 18. Juni nicht ausgewählt werden kann;
Wir duplizieren [app-10.html] in [app-11.html] und nehmen folgende Änderungen vor:
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
...
</head>
<body ng-controller="rdvMedecinsCtrl">
<div class="container">
<h1>Rdvmedecins - v1</h1>
<div>
<pre>Date <em>{{jour | date:'fullDate' }}</em></pre>
<div class="row">
<div class="col-md-2">
<h4>Calendrier</h4>
<div style="display:inline-block; min-height:290px;">
<datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
</div>
</div>
</div>
</div>
</div>
<!-- Bootstrap-Kern JavaScript ================================================== -->
...
<!-- Lokales Skript -->
<script>
// --------------------- Angular-Modul
angular.module("rdvmedecins", ['ui.bootstrap']);
// Controller
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope',
function ($scope) {
// Mindestdatum
$scope.minDate = new Date();
}]);
</script>
</body>
</html>
Betrachten wir zunächst das lokale Skript in den Zeilen 26–37:
- Zeile 28: Erstellung des Moduls [rdvmedecins] mit seiner Abhängigkeit vom Modul [ui.bootstrap], das den Kalender bereitstellt;
- Zeilen 30–35: Erstellung eines Controllers. Dieser wird das Template unserer Seite verwalten. Hier wird es keinen Ereignishandler geben;
- Zeilen 30–31: Der Controller [rdvMedecinsCtrl] gehört zum Modul [rdvmedecins]. Einem Modul können beliebig viele Controller hinzugefügt werden. In unserer Anwendung gibt es:
- ein Modul zur Verwaltung der Anwendung;
- einen Controller pro Ansicht;
- Der zweite Parameter der Funktion [controller] ist ein Array der Form ['O1', 'O2', ..., 'On', function(O1, O2, ..., On)]. Der letzte Parameter ist die Funktion, die den Controller implementiert. Ihre Parameter sind Objekte, die Angular JS der Funktion bereitstellt.
Kommen wir zurück zur Architektur einer Angular-Anwendung:
![]() |
Oben enthält der Controller C1 alle Ereignisbehandler der Ansicht V1 sowie deren Modell M1. Die Ereignisbehandler benötigen möglicherweise einen oder mehrere Dienste [6], um ihre Aufgabe zu erfüllen. Diese werden alle als Parameter an die Konstruktorfunktion des Controllers übergeben:
Die Si-Dienste sind Singletons. Angular erstellt jeweils nur ein einziges Exemplar davon. Sie werden durch einen Si-Namen identifiziert. Warum tauchen sie in der obigen Tabelle zweimal auf? Im Betrieb werden die Skripte JS minimiert. Bei diesem Minimierungsprozess sieht die obige Tabelle wie folgt aus:
Die Parameter verlieren ihre Namen. Dabei handelt es sich jedoch um die Namen von Diensten. Es ist daher wichtig, diese Namen beizubehalten. Aus diesem Grund werden sie als Zeichenketten als Parameter vor der Funktion übergeben. Die Zeichenketten werden beim Minifizierungsprozess nicht verändert. Wenn Angular den Controller mit dem neuen Array erstellt, ersetzt es a1 durch S1, a2 durch S2, … Die Reihenfolge der Parameter ist daher wichtig. Sie muss mit der Reihenfolge der Dienste übereinstimmen, die der Definition der Funktion vorangestellt sind.
Kehren wir zur Definition des Controllers „[rdvMedecinsCtrl]“ zurück:
// Controller
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope',
function ($scope) {
// Mindestdatum
$scope.minDate = new Date();
}]);
- Zeilen 3–4: Das einzige Objekt, das in den Controller injiziert wird, ist das Objekt $scope. Dabei handelt es sich um ein vordefiniertes Objekt, das das Modell M der mit dem Controller verbundenen Ansichten darstellt. Um das Modell einer Ansicht zu erweitern, genügt es, dem Objekt $scope Felder hinzuzufügen;
- was in Zeile 6 geschieht. Es wird das Feld [minDate] mit dem heutigen Datum als Wert angelegt;
Die Ansicht V nutzt dieses Modell M wie folgt:
<body ng-controller="rdvMedecinsCtrl">
<div class="container">
...
<div style="display:inline-block; min-height:290px;">
<datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
</div>
...
</div>
...
- Zeile 1: Der Hauptteil der Seite wird über das Attribut [ng-controller] dem Controller [rdvMedecinsCtrl] zugeordnet. Das bedeutet, dass alles, was sich innerhalb des Tags <body> befindet, den Controller [rdvMedecinsCtrl] verwendet, um seine Ereignisse zu verwalten und sein M-Modell abzurufen. Eine Seite HTML kann von mehreren Controllern abhängen, die entweder ineinander verschachtelt sind oder nicht:
Oben:
- Der Inhalt von [div1] (Zeilen 1–10) zeigt die Vorlage M1 an, die vom Controller c1 verwaltet wird. Die Tags in diesem Bereich können auf Ereignisbehandler des Controllers c1 verweisen;
- Der Inhalt von [div11] (Zeilen 3–4) zeigt die Vorlage M11 an, die vom Controller c11 verwaltet wird, aber auch die Vorlage M1. Es findet eine Vererbung der Vorlagen statt. Die Tags in diesem Bereich können sowohl auf Ereignisbehandler des Controllers c11 als auch auf Ereignisbehandler des Controllers c1 verweisen. Sie dürfen weder auf die Vorlage M12 des Controllers c12 noch auf dessen Ereignisbehandler verweisen. Der Controller c12 ist in den Zeilen 3–5 nämlich nicht bekannt;
- Zeilen 7–9: Hier lässt sich eine ähnliche Argumentation wie zuvor anstellen;
Kehren wir zum Kalender-Code zurück:
<datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
Das Attribut [min-date] wird mit dem Wert [minDate] aus dem Modell initialisiert. Implizit [$scope.minDate]. Das Feld wird immer im Objekt $scope gesucht.
3.7.2. Beispiel 2: Lokalisierung von Datumsangaben
Derzeit ist der Kalender für uns kaum von Nutzen, da es sich um einen englischen Kalender handelt. Es ist möglich, ihn zu lokalisieren:
![]() |
- In [1] haben wir einen Kalender auf Französisch;
- mit [2] wird er auf Englisch umgestellt;
- mit [3] den englischen Kalender;
Wir duplizieren die Seite [app-11.html] in [app-12.html] und ändern diese anschließend wie folgt:
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
...
</head>
<body ng-controller="rdvMedecinsCtrl">
<div class="container">
<h1>Rdvmedecins - v1</h1>
<pre>Date <em>{{jour | date:'fullDate' }}</em></pre>
<div class="row">
<!-- der Kalender-->
<div class="col-md-4">
<h4>Calendrier</h4>
<div style="display:inline-block; min-height:290px;">
<datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
</div>
</div>
<!-- Sprachen -->
<div class="col-md-2">
<div class="btn-group" dropdown is-open="isopen">
<button type="button" class="btn btn-primary dropdown-toggle" style="margin-top: 30px">
Langues<span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li><a href="" ng-click="setLang('fr')">Français</a></li>
<li><a href="" ng-click="setLang('en')">English</a></li>
</ul>
</div>
</div>
</div>
</div>
...
<script type="text/javascript" src="rdvmedecins.js"></script>
</body>
</html>
Es gibt nur wenige Änderungen. Es wurden lediglich die Zeilen 21–31 für die Sprachauswahlliste hinzugefügt. Zum ersten Mal stoßen wir in den Zeilen 27–28 auf einen Ereignis-Handler:
- Zeile 27: Das Attribut [ng-click] ist ein Angular-Attribut, das den Ereignis-Handler angibt, der ausgeführt werden soll, wenn auf das Element mit diesem Attribut geklickt wird. Hier wird die Funktion [$scope.setLang('fr')] ausgeführt. Sie stellt den Kalender auf Französisch um;
- Zeile 28: Hier wird der Kalender auf Englisch umgestellt;
- Zeile 35: Da das JavaScript des Controllers recht umfangreich ist, legen wir es in einer Datei namens [rdvmedecins.js] ab;
Angular verwaltet die Lokalisierung der Ansichten mit einem Modul namens [ngLocale]. Die Definition unseres Moduls [rdvmedecins] lautet daher wie folgt:
// --------------------- Angular-Modul
angular.module("rdvmedecins", ['ui.bootstrap', 'ngLocale']);
In Zeile 2 dürfen die Abhängigkeiten nicht vergessen werden, da Angular in seinen Fehlermeldungen manchmal nicht sehr präzise ist. Das Auslassen einer Abhängigkeit ist daher besonders schwer zu erkennen. Hier haben wir eine neue Abhängigkeit vom Modul [ngLocale].
Standardmäßig unterstützt Angular nur die Lokalisierung von Datumsangaben, Zahlen usw., für die es lokale Varianten gibt. Die Internationalisierung von Texten wird nicht unterstützt. Hierfür verwenden wir die Bibliothek [angular-translate]. Die Lokalisierung wird von der Bibliothek [angular-i18n] übernommen. Diese Bibliothek enthält so viele Dateien, wie es Varianten für Datumsangaben, Zahlen usw. gibt.
![]() |
Für den französischen Kalender verwenden wir die Datei [angular-locale_fr-fr.js] und für den englischen Kalender die Datei [angular-locale_en-us.js]. Schauen wir uns beispielsweise an, was in der Datei [angular-locale_fr-fr.js] enthalten ist:
'use strict';
angular.module("ngLocale", [], ["$provide", function($provide) {
var PLURAL_CATEGORY = {ZERO: "zero", ONE: "one", TWO: "two", FEW: "few", MANY: "many", OTHER: "other"};
$provide.value("$locale", {
"DATETIME_FORMATS": {
"AMPMS": [
"AM",
"PM"
],
"DAY": [
"dimanche",
"lundi",
"mardi",
"mercredi",
"jeudi",
"vendredi",
"samedi"
],
"MONTH": [
"janvier",
"f\u00e9vrier",
"mars",
"avril",
"mai",
"juin",
"juillet",
"ao\u00fbt",
"septembre",
"octobre",
"novembre",
"d\u00e9cembre"
],
"SHORTDAY": [
"dim.",
"lun.",
"mar.",
"mer.",
"jeu.",
"ven.",
"sam."
],
"SHORTMONTH": [
"janv.",
"f\u00e9vr.",
"mars",
"avr.",
"mai",
"juin",
"juil.",
"ao\u00fbt",
"sept.",
"oct.",
"nov.",
"d\u00e9c."
],
"fullDate": "EEEE d MMMM y",
"longDate": "d MMMM y",
"medium": "d MMM y HH:mm:ss",
"mediumDate": "d MMM y",
"mediumTime": "HH:mm:ss",
"short": "dd/MM/yy HH:mm",
"shortDate": "dd/MM/yy",
"shortTime": "HH:mm"
},
"NUMBER_FORMATS": {
"CURRENCY_SYM": "\u20ac",
"DECIMAL_SEP": ",",
"GROUP_SEP": "\u00a0",
"PATTERNS": [
{
"gSize": 3,
"lgSize": 3,
"macFrac": 0,
"maxFrac": 3,
"minFrac": 0,
"minInt": 1,
"negPre": "-",
"negSuf": "",
"posPre": "",
"posSuf": ""
},
{
"gSize": 3,
"lgSize": 3,
"macFrac": 0,
"maxFrac": 2,
"minFrac": 2,
"minInt": 1,
"negPre": "(",
"negSuf": "\u00a0\u00a4)",
"posPre": "",
"posSuf": "\u00a0\u00a4"
}
]
},
"id": "fr-fr",
"pluralCat": function (n) { if (n >= 0 && n <= 2 && n != 2) { return PLURAL_CATEGORY.ONE; } return PLURAL_CATEGORY.OTHER;}
});
}]);
Darin sind die Elemente zu sehen, mit denen ein französischer Kalender erstellt werden kann:
- Zeilen 10–18: die Tabelle der Wochentage;
- Zeilen 19–32: die Tabelle der Monate des Jahres;
- Zeilen 33–41: die Tabelle der Wochentage in Kurzform;
- Zeilen 42–55: die Tabelle der Monatsnamen in Kurzform;
- Zeilen 56–63: Datums- und Zeitformate. In Zeile 62 erkennt man das Format „TT/MM/JJ“ für französische Datumsangaben;
- Zeilen 65–95: Informationen zur Formatierung von Zahlen. Das interessiert uns hier nicht;
- Zeile 96: die Kennung „fr-fr“ der Ländereinstellung der Datei (fr-fr: Französisch (Frankreich), fr-ca: Französisch (Kanada), …)
In der Datei „[angular-locale_en-us.js]“ findet sich genau dasselbe, diesmal jedoch für die englische Version von „USA“ (en-us).
Der obige Code ist nicht besonders leicht zu lesen. Bei genauerer Betrachtung stellt man fest, dass der gesamte Code die Variable [$locale] in Zeile 4 definiert. Durch Ändern des Werts dieser Variablen wird die Internationalisierung von Datumsangaben, Zahlen, Währungen usw. erreicht. Seltsamerweise hat Angular nicht vorgesehen, dass die Variable [$locale] während der Laufzeit geändert wird. Sie wird ein für alle Mal definiert, indem die Datei der gewünschten Locale importiert wird:
<script type="text/javascript" src="bower_components/angular-i18n/angular-locale_fr-fr.js"></script>
Es macht keinen Sinn, alle Dateien der gewünschten Locales zu importieren, da jede Datei, wie wir gesehen haben, nur eine einzige Aufgabe erfüllt: die Variable [$locale] zu definieren. Es gilt die zuletzt importierte Datei, und es gibt anschließend keine Möglichkeit mehr, die Locale zu ändern.
Beim Stöbern im Internet auf der Suche nach einer Lösung für dieses Problem bin ich nicht fündig geworden. Hier schlage ich eine Lösung vor: [https://github.com/stahe/angular-ui-bootstrap-datepicker-with-locale-updated-on-the-fly]. Die Idee besteht darin, die verschiedenen benötigten Sprachversionen in ein Wörterbuch zu speichern. Dort werden wir sie abrufen, wenn eine Änderung erforderlich ist. Der JavaScript-Code von [rdvmedecins.js] hat folgenden Aufbau:
![]() |
Wenn man die Definition der Ländereinstellungen weglässt, die 200 Zeilen umfasst (Zeilen 15–215 oben), ist der Code einfach:
- Zeile 6: definiert das Modul [rdvmedecins] und seine Abhängigkeiten;
- Zeilen 8–10: Definiert den Controller [rdvMedecinsCtrl] der Seite;
- Zeile 9: Die Konstruktorfunktion des Controllers erhält zwei Parameter:
- $scope: zum Erstellen der Ansicht;
- $locale: Dies ist die Variable, die die Lokalisierung des Kalenders verwaltet. Sie muss geändert werden, wenn die Sprache gewechselt wird;
- Zeile 13: Die Variable [minDate] der Vorlage wird mit dem aktuellen Datum initialisiert;
- Zeile 15: definiert das Wörterbuch [locales]. Beachten Sie, dass wir nicht [$scope.locales] geschrieben haben. Die Variable [locales] ist nämlich nicht Teil der Vorlage, die der Ansicht zur Verfügung gestellt wird;
- Zeilen 15–215: definieren ein Wörterbuch {'fr':locale-fr-fr, 'en':locale-en-us}. Die Werte [locale-fr-fr] und [locale-en-us] werden jeweils aus den Dateien JS, [angular-locale_fr-fr.js] und [angular-locale_en-us.js] entnommen. Das Schwierigste ist, sich bei den sehr vielen Klammern in diesem Wörterbuch nicht zu vertun...
- Zeile 217: Die Variable $locale wird mit locales['fr'] initialisiert, d. h. mit der französischen Version der Locale. Man kann nicht einfach [$locale=locales['fr']] schreiben, da dies $locale die Adresse von locales['fr'] zuweisen würde. Man muss eine Wertkopie erstellen. Dies kann mit der vordefinierten Funktion [angular.copy] erfolgen;
- Zeile 219: Die Variable [jour] des Modells wird mit dem aktuellen Datum initialisiert. Dies führt dazu, dass der Kalender auf dieses Datum eingestellt angezeigt wird;
- Zeilen 223–230: Definieren den Ereignishandler, der beim Sprachwechsel aufgerufen wird. Beachten Sie die Syntax:
um einen Ereignis-Handler zu definieren, der [nom_fonction] heißen würde und die Parameter [param1, param2, ...] akzeptieren würde;
Erinnern wir uns an den Code HTML aus der Dropdown-Liste:
<!-- Sprachen -->
<div class="col-md-2">
<div class="btn-group" dropdown is-open="isopen">
<button type="button" class="btn btn-primary dropdown-toggle" style="margin-top: 30px">
Langues<span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li><a href="" ng-click="setLang('fr')">Français</a></li>
<li><a href="" ng-click="setLang('en')">English</a></li>
</ul>
</div>
</div>
- Zeile 8: Die Auswahl von Französisch führt zum Aufruf von [setLang('fr')];
- Zeile 9: Die Auswahl von Englisch führt zum Aufruf von [setLang('en')];
- Zeile 3: Das Attribut [is-open] ist ein boolescher Wert, der steuert, ob die Dropdown-Liste geöffnet (true) oder geschlossen (false) ist. Es wird mit der Variablen [isopen] aus dem View-Modell initialisiert;
Kehren wir zum Code von [rdvmedecins.js] zurück:
- Zeile 225: Der Wert der Variablen [$locale] wird durch den entsprechenden Wert aus dem Dictionary [locales] ersetzt;
- Zeile 227: Wir haben festgelegt, dass die Ansicht V automatisch mit dem neuen Modell aktualisiert wird, sobald sich das Modell M einer Ansicht V ändert. In Zeile 225 wurde der Wert der Variablen [$locale] geändert, die nicht Teil des von der Ansicht V angezeigten Modells M ist. Es muss ein Weg gefunden werden, dieses Modell M so zu ändern, dass der Kalender aktualisiert wird und seine neue Ländereinstellung verwendet. Hier ändern wir die Variable [jour] des Kalendermodells. Man initialisiert sie mit einem neuen Zeiger (new), der auf ein Datum verweist, das mit dem angezeigten identisch ist. [$scope.jour.getTime()] ist die Anzahl der Millisekunden, die zwischen dem 1. Januar 1970 und dem vom Kalender angezeigten Datum verstrichen sind. Mit dieser Zahl wird ein neues Datum rekonstruiert. Natürlich erhalten wir dasselbe Datum zurück, und der Kalender bleibt auf dem zuvor angezeigten Datum stehen. Der Wert von [$scope.jour], der eigentlich ein Zeiger ist, hat sich jedoch geändert, und der Kalender wird aktualisiert;
- Zeile 229: Der Wert der Variablen [isopen] aus der Vorlage wird auf false gesetzt. Diese Variable steuert eines der Attribute der Dropdown-Liste:
<div class="btn-group" dropdown is-open="isopen">
<button type="button" class="btn btn-primary dropdown-toggle" style="margin-top: 30px">
Langues<span class="caret"></span>
</button>
...
</div>
In Zeile 1 oben wird das Attribut [is-open] auf false gesetzt, wodurch die Dropdown-Liste geschlossen wird.
3.7.3. Beispiel 3: Internationalisierung von Texten
Kehren wir zur Lokalisierung des Kalenders zurück:
![]() |
Bei [3] sehen wir, dass der Kalender auf Englisch ist, die Texte unter [Calendrier, Langues] jedoch nicht. Standardmäßig bietet Angular kein Tool zur Internationalisierung von Meldungen. Wir werden hier die Bibliothek [angular-translate] (https://github.com/angular-translate/angular-translate) verwenden.
Wir werden das folgende Beispiel entwickeln:
![]() |
- in [1], die Ansicht auf Französisch;
- in [2], die Ansicht auf Englisch;
Sehen wir uns die für die Internationalisierung erforderliche Konfiguration an. Das Skript [rdvmedecins.js] wird wie folgt geändert:
// --------------------- Angular-Modul
angular.module("rdvmedecins", ['ui.bootstrap', 'ngLocale', 'pascalprecht.translate']);
// i18n-Konfiguration
angular.module("rdvmedecins")
.config(['$translateProvider', function ($translateProvider) {
// Meldungen auf Französisch
$translateProvider.translations("fr", {
'msg_header': 'Arztpraxis <br/> Les Médecins Associés',
'msg_langues': 'Sprachen',
'msg_agenda': 'Terminkalender von {{Titel}} {{Vorname}} {{Nachname}}<br/>am {{Tag}}',
'msg_calendrier': 'Kalender',
'msg_jour': 'Ausgewählter Tag: ',
'msg_meteo': „Heute wird es regnen...“
});
// englische Meldungen
$translateProvider.translations("en", {
'msg_header': 'The Associated Doctors',
'msg_langues': 'Sprachen',
'msg_agenda': „{{Titel}} {{Vorname}} {{Nachname}}s Tagebuch<br/> am {{Tag}}“,
'msg_calendrier': 'Kalender',
'msg_jour': 'Ausgewählter Tag: ',
'msg_meteo': 'Heute wird es regnen...'
});
// Standardsprache
$translateProvider.preferredLanguage("fr");
}]);
- Zeile 2: Die erste Änderung ist das Hinzufügen einer neuen Abhängigkeit. Für die Internationalisierung der Anwendung wird das Angular-Modul [pascalprecht.translate] benötigt;
- Zeilen 5–26: Hier wird die Funktion [config] des Moduls [rdvmedecins] definiert. Beim Start einer Angular-Anwendung instanziiert das Framework alle für die Anwendung erforderlichen Dienste, sowohl die vordefinierten von Angular als auch die benutzerdefinierten. Bislang haben wir noch keine Dienste definiert. Die Funktion [config] des Anwendungsmoduls wird vor jeder Instanziierung eines Dienstes ausgeführt. Sie kann verwendet werden, um Konfigurationsinformationen für die Dienste festzulegen, die anschließend instanziiert werden. Hier wird die Funktion [config] verwendet, um die internationalisierten Meldungen der Anwendung zu definieren;
- Zeile 5: Der Parameter der Funktion [config] ist ein Array ['O1', 'O2', ..., 'On', function(O1, O2, ..., On)], wobei Oi ein bekanntes, von Angular bereitgestelltes Objekt ist. Hier wird das Objekt [$translateProvider] vom Modul [pascalprecht.translate] bereitgestellt. [function] ist die Funktion, die zur Konfiguration der Anwendung ausgeführt wird;
- Zeilen 7–14: Die Funktion [$translateProvider.translations] akzeptiert zwei Parameter:
- Der erste Parameter ist der Schlüssel einer Sprache. Hier kann man beliebige Werte eingeben. In diesem Fall haben wir „fr“ für die französischen Übersetzungen (Zeile 7) und „en“ für die englischen Übersetzungen (Zeile 16) verwendet,
- der zweite ist die Liste der Übersetzungen in Form eines Wörterbuchs {'cle1':'msg1', 'cle2':'msg2', ...};
- Zeilen 7–14: die französischen Meldungen;
- Zeilen 16–23: die englischen Meldungen;
- Zeile 25: Die Methode [preferredLanguage] legt die Standardsprache fest. Ihr Parameter ist eines der Argumente, die als erster Parameter der Funktion [$translateProvider.translations] verwendet werden, also hier entweder „fr“ (Zeile 7) oder „en“ (Zeile 16);
- Es sei angemerkt, dass es drei Arten von Meldungen gibt:
- Meldungen ohne Parameter und ohne Elemente von HTML (Zeilen 9, 11, 12, …),
- Meldungen mit HTML-Elementen (Zeilen 8, 10, ...),
- Meldungen mit Parametern (Zeilen 10, 19);
Wir duplizieren nun [app-11.html] in [app-12.html] und nehmen folgende Änderungen vor:
<div class="container">
<!-- ein erster Text mit HTML-Elementen darin -->
<h3 class="alert alert-info" translate="{{'msg_header'}}"></h3>
<!-- ein zweiter Text mit Parametern -->
<h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>
<!-- ein dritter Text, der vom Controller übersetzt wurde -->
<h3 class="alert alert-danger">{{msg2}}</h3>
<pre>{{'msg_jour'|translate}}<em>{{jour | date:'fullDate' }}</em></pre>
<div class="row">
<!-- der Kalender-->
<div class="col-md-4">
<h4>{{'msg_calendrier'|translate}}</h4>
<div style="display:inline-block; min-height:290px;">
<datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
</div>
</div>
<!-- die Sprachen -->
<div class="col-md-2">
<div class="btn-group" dropdown is-open="isopen">
<button type="button" class="btn btn-primary dropdown-toggle" style="margin-top: 30px">
{{'msg_langues'|translate}}<span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li><a href="" ng-click="setLang('fr')">Français</a></li>
<li><a href="" ng-click="setLang('en')">English</a></li>
</ul>
</div>
</div>
</div>
</div>
- Die Übersetzungen erfolgen in den Zeilen 3, 5, 9, 13, 23;
- Es lassen sich drei Syntaxen unterscheiden:
- die Syntax [translate={{'msg_key'}}] (Zeile 3), wobei [msg_key] einer der Schlüssel eines Übersetzungswörterbuchs ist. Diese Syntax eignet sich für Nachrichten mit oder ohne Elemente HTML, jedoch nicht für solche mit Parametern;
- die Syntax [translate={{'msg_key'}} translate-values={{dictionnaire]}}] (Zeile 5) eignet sich für Nachrichten mit oder ohne HTML-Elemente und mit Parametern;
- Die Syntax [{{'msg_key'|translate}}] (Zeilen 9, 13, 23) eignet sich für Nachrichten ohne Parameter und ohne HTML-Elemente;
Sehen wir uns die verschiedenen Nachrichten in dieser Ansicht an:
Arztpraxis <br/> Les Médecins Associés | Die assoziierten Ärzte | |
Kalender | Kalender | |
Sprachen | Languages | |
Ausgewählter Tag: | Ausgewählter Tag: |
Betrachten wir nun Zeile 5:
<h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>
Es ist zu beachten, dass [msg.text] und [msg.model] nicht in Anführungszeichen stehen. Es handelt sich dabei nicht um Zeichenfolgen, sondern um Elemente der Vorlage:
- msg.text: definiert den zu verwendenden Schlüssel der parametrisierten Nachricht;
- msg.model: ist das Wörterbuch, das die Werte der Parameter bereitstellt;
Die Feldnamen [text, model] können beliebig gewählt werden. Im Controller [rdvMedecinsCtrl] der Ansicht wird das Objekt [msg] wie folgt definiert:

- Zeile 245: die Definition des Objekts [msg];
- Zeile 245: Das Feld [text] hat den Schlüssel [msg_agenda] als Wert, der zwei Werten zugeordnet ist:
- Tagebuch von {{Titel}} {{Vorname}} {{Nachname}}<br/>am {{Tag}} im französischen Wörterbuch;
- {{Titel}} {{Vorname}} {{Nachname}}'s Diary<br/> am {{Tag}} im englischen Wörterbuch;
Die anzuzeigende Meldung hat also vier Parameter: [titre, prenom, nom, jour];
- Zeile 245: Das Feld [model] ist ein Wörterbuch, das diesen vier Parametern einen Wert zuweist. Beim Parameter [jour] gibt es eine Schwierigkeit. Wir möchten den vollständigen Namen des Tages anzeigen. Dieser ist je nach Sprache (Französisch oder Englisch) unterschiedlich. Daher verwenden wir den Filter [date], der bereits in der Ansicht in der Form {{ Tag | date:'fullDate'}} verwendet wurde. Es ist möglich, jeden Filter im JavaScript-Code in der Form $filter('filter')(Wert, Ergänzungen) zu verwenden, wobei $filter ein vordefiniertes Angular-Objekt und 'filter' der Name des Filters ist;
- Zeilen 33–34: Das vordefinierte Objekt $filter wird als Parameter an den Controller übergeben, wodurch es in Zeile 245 verwendet werden kann;
Kehren wir zu einer anderen Zeile der angezeigten Ansicht zurück:
<!-- ein dritter, vom Controller übersetzter Text -->
<h3 class="alert alert-danger">{{msg2}}</h3>
Alle bisherigen Übersetzungen wurden in der Ansicht mithilfe von Attributen des Moduls [pascalprecht.translate] vorgenommen. Man kann sich aber auch dafür entscheiden, diese Übersetzung serverseitig durchzuführen. Genau das geschieht hier. Im Controller (Zeile 247 im obigen Screenshot) befindet sich der folgende Code:
$scope.msg2 = $filter('translate')('msg_meteo');
Es wird dieselbe Syntax wie für den Filter „date“ verwendet, da „translate“ ebenfalls ein Filter ist. Hier wird die Schlüsselmeldung „msg_meteo“ angefordert.
Betrachten wir den Mechanismus der Sprachumschaltung. Wir haben gesehen, dass die Konfigurationsfunktion [config] des Moduls [rdvmedecins] Französisch als Standardsprache festgelegt hat (Zeile 9 unten):
// i18n-Konfiguration
angular.module("rdvmedecins")
.config(['$translateProvider', function ($translateProvider) {
// Meldungen auf Französisch
$translateProvider.translations("fr", {...});
// englische Meldungen
$translateProvider.translations("en", {...});
// Standardsprache
$translateProvider.preferredLanguage("fr");
}]);
Wir erinnern außerdem daran, dass die Standard-Locale ebenfalls Französisch war. Bei der Initialisierung des Controllers [rdvmedecins] wurde Folgendes geschrieben:
// Die Ländereinstellung wird auf Französisch gesetzt
angular.copy(locales['fr'], $locale);
- Zeile 2: [locales] ist ein Wörterbuch, das wir erstellt haben;
Es besteht kein Zusammenhang zwischen der durch das Modul [pascalprecht.translate] ermöglichten Internationalisierung von Nachrichten und der von uns implementierten Lokalisierung von Datumsangaben. Letztere verwendet eine Variable $locale, die vom Modul [pascalprecht.translate] nicht verwendet wird. Es handelt sich um zwei Prozesse, die sich gegenseitig nicht beeinflussen.
Nun ist es an der Zeit zu untersuchen, was passiert, wenn der Benutzer die Sprache wechselt:

- Zeile 251: Bei einem Sprachwechsel wird die Funktion [setLang] mit einem der beiden Parameter ['fr','en'] aufgerufen;
- Zeilen 252–257: wurden bereits erläutert – sie ändern die Variable [$locale] des Kalenders. Dies hat keinerlei Auswirkungen auf die Sprache der Übersetzungen;
- Zeile 259: Hier wird die Sprache der Übersetzungen geändert. Dazu wird das vom Modul [pascalprecht.translate] bereitgestellte Objekt [$translate] verwendet. Dazu muss es in den Controller eingefügt werden:
// Controller
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', '$locale', '$translate', '$filter',
function ($scope, $locale, $translate, $filter) {
In den Zeilen 3 und 4 oben wird das Objekt $translate eingefügt;
- Der Parameter „lang“ der Funktion [$translate.use(lang)] muss einen der Schlüssel als Wert haben, die in der Konfiguration als erster Parameter der Funktion [$translateProvider.translations] verwendet werden, d. h. entweder „fr“ oder „en“. Dies ist der Fall;
- Zeile 261: Der Wert von `msg2` wird neu berechnet. Warum? In der Ansicht werden nach dem in Zeile 259 durchgeführten Sprachwechsel alle vorhandenen Attribute von `[translate]` neu ausgewertet. Dies gilt jedoch nicht für den Ausdruck {{msg2}}, der dieses Attribut nicht besitzt. Daher wird sein neuer Wert im Controller berechnet. Dies muss nach dem Sprachwechsel in Zeile 259 erfolgen, damit die neue Sprache für die Berechnung von [msg2] verwendet wird;
Wenn man es dabei belässt, fallen zwei Unregelmäßigkeiten auf:
![]() |
- in [1] ist der Tag weiterhin auf Französisch, während der Rest der Ansicht auf Englisch ist;
- in [2] und [3] ist der ausgewählte Tag der 24. Juni, während in [1] der Tag weiterhin auf den 20. Juni festgelegt ist;
Versuchen wir zunächst, Erklärungen zu finden, bevor wir nach Lösungen suchen. Die Meldung [1] wird im Controller mit dem folgenden Code erstellt:
$scope.msg = {'text': 'msg_agenda', 'model': {'titre': 'Mme', 'prenom': 'Laure', 'nom': 'PELISSIER', 'jour': $filter('date')($scope.jour, 'fullDate')}};
und in der Ansicht mit folgendem Code angezeigt:
<h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>
Die Anomalie [1] (der Tag blieb auf Französisch, während der Rest der Ansicht auf Englisch ist) scheint zu zeigen, dass zwar das Attribut [translate] bei einem Sprachwechsel neu ausgewertet wird, das Attribut [translate-values] nicht neu ausgewertet wurde. Diese Neuberechnung kann dann im Controller erzwungen werden:
// ------------------- Ereignis-Manager
// Sprachwechsel
$scope.setLang = function (lang) {
...
// msg2 wird aktualisiert
$scope.msg2 = $filter('translate')('msg_meteo');
// und das Datum der Nachricht
$scope.msg.model.jour = $filter('date')($scope.jour, 'fullDate');
};
Bei jedem Sprachwechsel berechnet Zeile 8 oben den angezeigten Tag neu. Dies behebt zwar das erste Problem, nicht jedoch das zweite (der in der Meldung angezeigte Tag ändert sich nicht, wenn im Kalender ein anderer Tag ausgewählt wird). Der Grund für dieses Verhalten ist folgender: Die Meldung wird in der Ansicht mit dem folgenden Code angezeigt:
<h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>
Die angezeigte Ansicht V ändert sich nur, wenn sich ihr Modell M ändert. Hier löst jedoch die Auswahl eines neuen Tages im Kalender ein Ereignis aus, das nicht verarbeitet wird, was dazu führt, dass sich das Modell [msg] nicht ändert und sich somit auch die Ansicht nicht ändert. Wir passen die Definition des Kalenders in der Ansicht an:
<datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"
ng-click="calendarClick()"></datepicker>
Oben legen wir fest, dass der Klick auf den Kalender von der Funktion [$scope.calendarClick] verarbeitet werden soll. Diese lautet wie folgt:

- Zeile 267: der Handler für den Klick auf den Kalender;
- Zeile 269: Die Aktualisierung des angezeigten Tages wird durch die Nachricht [msg] erzwungen;
3.7.4. Beispiel 4: Ein Konfigurationsdienst
Kehren wir zur Architektur einer Angular-Anwendung JS zurück:
![]() |
Wir werden uns hier mit dem Begriff „Service“ befassen. Es handelt sich um einen recht weit gefassten Begriff. Während die oben genannte Schicht [DAO] eindeutig ein Service ist, kann jedes Angular-Objekt zu einem Service werden:
- Ein Service folgt einer bestimmten Syntax. Er hat einen Namen, und Angular erkennt ihn anhand dieses Namens;
- ein Service kann von Angular in Controller und andere Services injiziert werden;
Einige der Dienste, die wir im Modul [rdvmedecins] konfigurieren werden, müssen eingerichtet werden. Da ein Dienst in einen anderen Dienst injiziert werden kann, ist es naheliegend, die Konfiguration in einem Dienst vorzunehmen, den wir [config] nennen werden, und diesen dann in die zu konfigurierenden Dienste und Controller zu injizieren. Wir beschreiben nun diesen Vorgang.
Wir duplizieren [app-13.html] in [app-14.html] und nehmen folgende Änderungen vor:
<div class="container">
<!-- Überprüfung der Wartemeldung -->
<label>
<input type="checkbox" ng-model="waiting.visible">
<span>Voir le message d'attente</span>
</label>
<!-- die Wartemeldung -->
<div class="alert alert-warning" ng-show="waiting.visible">
<h1>{{ waiting.text | translate}}
<button class="btn btn-primary pull-right" ng-click="waiting.cancel()">
{{'msg_cancel'|translate}}</button>
<img src="assets/images/waiting.gif" alt=""/>
</h1>
</div>
...
</div>
...
<script type="text/javascript" src="rdvmedecins-02.js"></script>
- Zeilen 3–6: Ein Kontrollkästchen, das steuert, ob die Wartemeldung in den Zeilen 9–15 angezeigt wird oder nicht. Der Wert des Kontrollkästchens wird in die Variable [waiting.visible] des Modells M der Ansicht V geschrieben. Dieser Wert ist true, wenn das Kontrollkästchen aktiviert ist, und false andernfalls. Dies funktioniert in beide Richtungen. Wenn wir der Variablen [waiting.visible] den Wert „true“ zuweisen, wird das Kontrollkästchen aktiviert. Es besteht eine bidirektionale Zuordnung zwischen der Ansicht V und ihrem Modell M;
- Zeilen 9–15: eine Wartemeldung mit einer Schaltfläche zum Abbrechen der Wartezeit (Zeile 11);
- Zeile 9: Die Meldung ist nur sichtbar, wenn die Variable [waiting.visible] den Wert „true“ hat. Wenn wir also das Kontrollkästchen in Zeile 4 aktivieren:
- wird der Wert true der Variablen [waiting.visible] zugewiesen (ng-model, Zeile 4);
- da sich das Modell M geändert hat, wird die Ansicht V automatisch neu berechnet. Die Wartemeldung wird dann sichtbar gemacht (ng-show, Zeile 9);
- Die Funktionsweise ist analog, wenn man das Kontrollkästchen in Zeile 4 deaktiviert: Die Wartemeldung wird ausgeblendet;
- Zeile 10: Die Wartemeldung wird übersetzt (Filter translate);
- Zeile 11: Beim Klicken auf die Schaltfläche wird die Methode [waiting.cancel()] ausgeführt (Attribut ng-click);
- Zeile 12: Die Beschriftung der Schaltfläche wird übersetzt;
- Zeile 19: Der JavaScript-Code der Anwendung wird in eine neue Datei „JS“ und „[rdvmedecins-02]“ ausgelagert, um den bereits geschriebenen Code, der nun neu organisiert werden muss, nicht zu verlieren;
Das ergibt folgende Ansicht:
![]() |
- in [1], Kontrollkästchen nicht aktiviert;
- in [2], Kontrollkästchen aktiviert;
Das Skript [rdvmedecins-02] ist eine Neuanordnung des Skripts [rdvmedecins]:

- Zeile 6: das Modul [rdvmedecins] der Anwendung;
- Zeilen 9–10: die Konfigurationsfunktion der Anwendung;
- Zeilen 38–39: der Dienst [config];
- Zeilen 283–284: der Controller [rdvMedecinsCtrl];
Zuvor hatten wir im Controller das Wörterbuch locales={'fr':..., 'en': ...} definiert, das 200 Zeilen umfasste. Dieses Wörterbuch ist eindeutig ein Konfigurationselement, daher wird es in den Dienst [config] in den Zeilen 38–39 migriert. Dieser Dienst ist wie folgt definiert:

- Zeilen 38–39: Es wird ein Service mit der Funktion [factory] des Objekts [angular.module] angelegt. Die Syntax dieser Funktion entspricht der der vorherigen: `factory('nom_service', ['O1','O2', ...., 'On', function (O1, O2, ..., On){...}])`, wobei die Oi die Namen von Objekten sind, die Angular bekannt sind (vordefiniert oder vom Entwickler erstellt) und die Angular als Parameter in die `factory`-Funktion einfügt. Da die Funktion hier keine Parameter hat, wurde eine kürzere, ebenfalls zulässige Syntax verwendet: `factory('nom_service', function (){...})]`;
- Zeile 40: Die Funktion [factory] muss den Service mithilfe eines Objekts implementieren, das sie zurückgibt. Dieses Objekt ist der Service. Deshalb wird die Funktion als „Factory“ (Objekt-Fabrik) bezeichnet;
Im Allgemeinen hat der Code eines Dienstes folgende Form:
Angular.module('nom_module')
.factory('nom_service',['O1','O2', ...., 'On', function (O1, O2, ..., On){
// Vorbereitung des Dienstes
...
// Das Objekt, das den Dienst implementiert, wird zurückgegeben
return {
// Felder
...
// Methoden
...
}
});
- Zeile 6: Es wird ein Objekt JS zurückgegeben, das sowohl Felder als auch Methoden enthalten kann. Letztere sorgen für die Bereitstellung des Dienstes;
Hier definiert der Service [config] nur Felder und keine Methoden. Hier werden alle Parameter hinterlegt, die in der Anwendung konfiguriert werden können:
- Zeilen 42–47: die Schlüssel der zu übersetzenden Meldungen;
- Zeilen 59–62: die URL der Anwendung;
- Zeilen 64–69: die URL des Remote-Webdienstes;
- Zeile 71: Ein Aufruf von HTTP an einen Webdienst, der nicht antwortet, kann lange dauern. Hier wird die maximale Wartezeit auf die Antwort des Webdienstes auf 1 Sekunde festgelegt. Nach Ablauf dieser Zeit schlägt der Aufruf HTTP fehl und es wird eine Ausnahme JS ausgelöst;
- Zeile 73: Vor jedem Aufruf des Servers wird eine Wartezeit simuliert, deren Dauer hier in Millisekunden festgelegt wird. Eine Wartezeit von 0 bedeutet, dass keine Wartezeit vorhanden ist. Die Anwendung wird so gestaltet, dass der Benutzer einen von ihm gestarteten Vorgang abbrechen kann. Damit dieser abgebrochen werden kann, muss er mindestens einige Sekunden dauern. Diese künstliche Wartezeit wird verwendet, um lang andauernde Vorgänge zu simulieren;
- Zeile 75: Im Modus [debug=true] werden in der aktuellen Ansicht zusätzliche Informationen angezeigt. Standardmäßig ist dieser Modus aktiviert. In der Produktion würde man dieses Feld auf false setzen;
- Zeilen 77–278: Das Wörterbuch der beiden Sprachumgebungen „fr“ und „en“. Es befand sich zuvor im Controller [rdvMedecinsCtrl];
Mit diesem Service entwickelt sich der Controller [rdvMedecinsCtrl] wie folgt weiter:

- Zeilen 284–285: Der Dienst [config] wird in den Controller eingefügt;
- Zeile 290: Das Wörterbuch [locales] befindet sich nun im Dienst [config] und nicht mehr im Controller;
- Zeile 294: Das Objekt [waiting], das die Anzeige der Wartemeldung steuert. Der Schlüssel der Wartemeldung befindet sich im Dienst [config] (Feld „text“). Standardmäßig ist die Wartemeldung ausgeblendet (Feld „visible“). Das Feld „cancel“ hat als Wert den Namen der Funktion in Zeile 316. Dieses Feld ist also eine Methode oder Funktion;
- Zeile 316: Die Funktion [cancel] ist privat (es wurde nicht $scope.cancel=function(){} geschrieben). Kehren wir zum Code der Abbrechen-Schaltfläche zurück:
<button class="btn btn-primary pull-right" ng-click="waiting.cancel()">
Wenn der Benutzer auf die Schaltfläche „Abbrechen“ klickt, wird die Methode [$scope.waiting.cancel()] aufgerufen. Letztendlich wird die private Funktion cancel in Zeile 316 ausgeführt. Sie blendet lediglich die Wartemeldung aus, indem sie die Variable des Modells [waiting.visible] (Zeile 318) auf „false“ setzt;
3.7.5. Beispiel 5: Asynchrone Programmierung
Wir stellen nun einen neuen Service mit einem neuen Konzept vor: der asynchronen Programmierung.
![]() |
Unsere Anwendung wird drei Dienste umfassen:
- [config]: der soeben vorgestellte Konfigurationsdienst;
- [utils]: ein Dienst mit Hilfsmethoden. Wir werden zwei davon vorstellen;
- [dao]: der Dienst für den Zugriff auf den Webdienst zur Terminvereinbarung. Wir werden ihn in Kürze vorstellen;
Wir werden die folgende Anwendung schreiben:
![]() |
![]() |
- Hier geht es darum, das Banner [2] für eine durch [1] festgelegte Zeit anzuzeigen. Die Wartezeit kann durch [3] abgebrochen werden.
Wir duplizieren [app-01.html] in [app-15.html] und ändern den Code wie folgt:
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
<title>RdvMedecins</title>
...
</head>
<body ng-controller="rdvMedecinsCtrl">
<div class="container">
<!-- die Wartemeldung -->
<div class="alert alert-warning" ng-show="waiting.visible" ng-cloak="">
<h1>{{ waiting.text | translate}}
<button class="btn btn-primary pull-right" ng-click="waiting.cancel()">{{'msg_cancel'|translate}}</button>
<img src="assets/images/waiting.gif" alt=""/>
</h1>
</div>
<!-- das Formular -->
<div class="alert alert-info" ng-hide="waiting.visible">
<div class="form-group">
<label for="waitingTime">{{waitingTimeText | translate}}</label>
<input type="text" id="waitingTime" ng-model="waiting.time"/>
</div>
<button class="btn btn-primary" ng-click="execute()">Exécuter</button>
</div>
</div>
..
<script type="text/javascript" src="rdvmedecins-03.js"></script>
</body>
</html>
- Zeile 11: Das Attribut [ng-cloak] verhindert die Anzeige des Feldes, bevor dessen Angular-Ausdrücke berechnet wurden. Dadurch wird vermieden, dass das Feld kurzzeitig angezeigt wird, bevor das Attribut [ng-show] ausgewertet wird, das tatsächlich dazu führt, dass es ausgeblendet wird;
- Zeile 22: Die Benutzereingabe (Wartezeit) wird im Modell [waiting.time] gespeichert (Attribut ng-model);
- Zeile 28: Die Seite verwendet ein neues Skript [rdvmedecins-03];
Das Skript [rdvmedecins-03] lautet wie folgt:

- Zeile 6: das Angular-Modul, das die Anwendung verwaltet;
- Zeile 10: Die Funktion [config], die zur Internationalisierung der Meldungen verwendet wird;
- Zeile 41: der von uns beschriebene Dienst [config];
- Zeile 286: der Service [utils], den wir erstellen werden;
- Zeile 315: der Controller [rdvmedecinsCtrl], den wir erstellen werden;
Wir fügen der Funktion [config] einen neuen Nachrichtenschlüssel hinzu (Zeilen 6, 11):
angular.module("rdvmedecins")
.config(['$translateProvider', function ($translateProvider) {
// Meldungen auf Französisch
$translateProvider.translations("fr", {
...
'msg_waiting_time_text': „Wartezeit: “
});
// englische Meldungen
$translateProvider.translations("en", {
...
'msg_waiting_time_text': „Waiting time:“
});
// Standardsprache
$translateProvider.preferredLanguage("fr");
}]);
Wir fügen dem Dienst [config] eine neue Zeile (Zeile 6) für diesen Nachrichtenschlüssel hinzu:
angular.module("rdvmedecins")
.factory('config', function () {
return {
// zu internationalisierende Meldungen
...
waitingTimeText: 'msg_waiting_time_text',
Der Service [utils] enthält zwei Methoden (Zeilen 4, 12):
angular.module("rdvmedecins")
.factory('utils', ['config', '$timeout', '$q', function (config, $timeout, $q) {
// Anzeige der JSON-Darstellung eines Objekts
function debug(message, data) {
if (config.debug) {
var text = data ? message + " : " + angular.toJson(data) : message;
console.log(text);
}
}
// Warten
function waitForSomeTime(milliseconds) {
// asynchrone Wartezeit in Millisekunden
var task = $q.defer();
$timeout(function () {
task.resolve();
}, milliseconds);
// Die Aufgabe wird zurückgegeben
return task;
};
// Service-Instanz
return {
debug: debug,
waitForSomeTime: waitForSomeTime
}
}]);
- Zeile 2: Der Dienst heißt [utils] (1. Parameter). Er hängt von drei Diensten ab: zwei vordefinierten Angular-Diensten ($timeout, $q) und dem Dienst „config“. Der Dienst [$timeout] ermöglicht es, eine Funktion auszuführen, nachdem eine bestimmte Zeit verstrichen ist. Der Dienst [$q] ermöglicht es, asynchrone Aufgaben zu erstellen;
- Zeile 4: eine lokale Funktion [debug];
- Zeile 12: eine lokale Funktion [waitForSomeTime];
- Zeilen 23–26: die Instanz des Dienstes [utils]. Dabei handelt es sich um ein Objekt, das zwei Methoden bereitstellt, nämlich die in den Zeilen 4 und 12. Beachten Sie, dass die Felder des Objekts beliebige Namen tragen können. Der Einheitlichkeit halber wurden sie nach den Funktionen benannt, auf die sie verweisen;
- Zeilen 4–9: Die Methode [debug] gibt eine Meldung [message] und gegebenenfalls die Darstellung JSON eines Objekts [data] auf der Konsole aus. Dadurch können Objekte beliebiger Komplexität angezeigt werden;
- Zeilen 12–20: Die Methode [waitForSomeTime] erstellt eine asynchrone Aufgabe, die [milliseconds] Millisekunden dauert;
- Zeile 14: Erstellung einer Aufgabe mithilfe des vordefinierten Objekts [$q] (https://docs.angularjs.org/api/ng/service/$q). Nachfolgend das API der Aufgabe, die in der Angular-Dokumentation als [deferred] bezeichnet wird:

- Eine asynchrone Aufgabe [task] wird durch die Anweisung [$q.defer()] erstellt;
- sie wird mit einer der beiden folgenden Methoden beendet:
- [task.resolve(value)]: Diese Methode beendet die Aufgabe erfolgreich und gibt den Wert [value] an diejenigen zurück, die auf den Abschluss der Aufgabe warten;
- [task.reject(value)]: Diese beendet die Aufgabe fehlgeschlagen und gibt den Wert [value] an diejenigen zurück, die auf das Ende der Aufgabe warten;
Die Aufgabe [task] kann denjenigen, die auf ihren Abschluss warten, regelmäßig Informationen übermitteln:
- [task.notify(value)]: sendet den Wert [value] an diejenigen, die auf das Ende der Aufgabe warten. Die Aufgabe wird weiter ausgeführt;
Diejenigen, die auf das Ende der Aufgabe warten möchten, nutzen das Feld [promise] dieser Aufgabe:
Das Objekt [promise] hat das folgende API (http://www.frangular.com/2012/12/api-promise-angularjs.html):

Um sowohl den Erfolg als auch das Scheitern der Aufgabe zu behandeln, schreibt man:
- Zeile 1: Man ruft das Promise der Aufgabe ab;
- Zeile 2: Man definiert die Funktionen, die im Erfolgs- bzw. im Fehlerfall ausgeführt werden sollen. Es ist möglich, keine Fehlerfunktion anzugeben. Die Funktion [successCallback] wird erst am Ende der Aufgabe [task] bei erfolgreichem Abschluss von [task.resolve()] ausgeführt. Die Funktion [errorCallBack] wird erst nach dem erfolgreichen Abschluss der Aufgabe [task] und dem Fehlschlag der Aufgabe [task.reject()] ausgeführt.
- Zeile 3: Hier wird die Funktion definiert, die ausgeführt werden soll, nachdem eine der beiden vorhergehenden Funktionen ausgeführt wurde. Hier wird der Code eingefügt, der beiden Funktionen gemeinsam ist: [successCallback, errorCallBack].
Kehren wir zum Code der Funktion [waitForSomeTime] zurück:
// Warten
function waitForSomeTime(milliseconds) {
// asynchrone Wartezeit in Millisekunden
var task = $q.defer();
$timeout(function () {
task.resolve();
}, milliseconds);
// Die Aufgabe wird zurückgegeben
return task;
};
- Zeile 4: Es wird eine Aufgabe angelegt;
- Zeilen 5–7: Mit dem Objekt [$timeout] lässt sich eine Funktion (1. Parameter) definieren, die nach einer bestimmten, in Millisekunden angegebenen Verzögerung (2. Parameter) ausgeführt wird. Hier ist der zweite Parameter der Funktion [$timeout] der Parameter der Methode (Zeile 1);
- Zeile 6: Nach Ablauf der Verzögerungszeit [milliseconds] wird die Aufgabe erfolgreich beendet;
- Zeile 9: Die Aufgabe [task] wird zurückgegeben. Dabei ist zu beachten, dass Zeile 9 unmittelbar nach der Definition des Objekts [$timeout] ausgeführt wird. Es wird nicht abgewartet, bis die Frist [milliseconds] abgelaufen ist. Der Code der Zeilen 2–10 wird also zu zwei verschiedenen Zeitpunkten ausgeführt:
- ein erstes Mal bei der Definition des Objekts [$timeout];
- ein zweites Mal, wenn die Verzögerung [milliseconds] abgelaufen ist;
Hier handelt es sich um eine asynchrone Funktion: Ihr Ergebnis wird zu einem späteren Zeitpunkt als dem ihrer Ausführung erhalten.
Der Code des Controllers, der den Dienst [config] nutzt, lautet wie folgt:
// Controller
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', '$filter',
function ($scope, utils, config, $filter) {
// ------------------- Modellinitialisierung
// Wartemeldung
$scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: undefined};
$scope.waitingTimeText = config.waitingTimeText;
// Warteaufgabe
var task;
// Protokolle
utils.debug("libellé temps d'attente", $filter('translate')($scope.waitingTimeText));
utils.debug("locales['fr']=", config.locales['fr']);
// Aktionsausführung
$scope.execute = function () {
// Protokoll
utils.debug('début', new Date());
// Wartemeldung wird angezeigt
$scope.waiting.visible = true;
// simulierte Wartezeit
task = utils.waitForSomeTime($scope.waiting.time);
// Wartezeit beendet
task.promise.then(function () {
// Erfolg
utils.debug('fin', new Date());
}, function () {
// Fehler
utils.debug('Opération annulée')
});
task.promise['finally'](function () {
// Warteende in allen Fällen
$scope.waiting.visible = false;
});
};
// Wartevorgang abgebrochen
function cancel() {
// Aufgabe wird beendet
task.reject();
}
}]);
- Zeile 3: Der Controller verwendet den Service [config];
- Zeile 7: Das Feld [time] wurde dem Objekt [$scope.waiting] hinzugefügt. Das Objekt [$scope.waiting.time] erhält den Wert der vom Benutzer festgelegten Wartezeit;
- Zeile 8: Der Schlüssel der von der Ansicht angezeigten Wartemeldung wird in das Modell [$scope.waitingTimeText] eingefügt. Generell muss alles, was von einer V-Ansicht angezeigt wird, in das Objekt [$scope] eingefügt werden;
- Zeile 10: Eine lokale Variable. Sie wird der Ansicht V nicht zur Verfügung gestellt;
- Zeilen 12–13: Aufruf der Methode [debug] des Dienstes [config]. Auf der Konsole wird folgendes Ergebnis angezeigt:
In Zeile 2 erhält man die Bezeichnung JSON für das Objekt locales['fr'].
- Zeile 16: Die Methode, die ausgeführt wird, wenn der Benutzer auf die Schaltfläche [Executer] klickt;
- Zeile 18: Zeigt den Startzeitpunkt der Methode an;
- Zeile 22: Die Aufgabe [waitForSomeTime] wird gestartet. Es wird nicht auf deren Ende gewartet. Die Ausführung wird mit der folgenden Zeile 24 fortgesetzt;
- Zeilen 24–30: Hier werden die Funktionen definiert, die bei erfolgreichem Abschluss der Aufgabe (Zeile 26) und im Fehlerfall (Zeile 29) ausgeführt werden sollen;
- Zeile 26: Zeigt den Zeitpunkt des Endes der Methodenausführung an;
- Zeile 29: Zeigt an, dass der Vorgang abgebrochen wurde. Dies geschieht nur, wenn der Benutzer auf die Schaltfläche [Annuler] klickt. Die Anweisung in Zeile 41 beendet dann die asynchrone Aufgabe mit einem Fehlercode;
- Zeilen 31–34: Hier wird die Funktion definiert, die nach der Ausführung einer der beiden vorherigen Funktionen ausgeführt werden soll;
Es ist wichtig, die Ausführungsabläufe dieses Codes zu verstehen. Falls der Benutzer eine Verzögerung von 3 Sekunden einstellt und die Wartezeit nicht abbricht:
- wird beim Klicken auf die Schaltfläche „[Exécuter]“ die Funktion „[$scope.execute]“ ausgeführt. Die Zeilen 16–34 werden ausgeführt, ohne dass die 3 Sekunden abgewartet werden. Am Ende dieser Ausführung wird die Ansicht V mit dem Modell M synchronisiert. Die Warteanzeige wird eingeblendet (ng-show=$scope.waiting.visible=true, Zeile 20) und das Formular wird ausgeblendet (ng-hide=$scope.waiting.visible=true, Zeile 20);
- ab diesem Zeitpunkt kann der Benutzer wieder mit der Ansicht interagieren. Er kann insbesondere auf die Schaltfläche [Annuler] klicken;
- tut er dies nicht, wird nach 3 Sekunden die Funktion von [$timeout] (siehe Zeilen 5–7 unten) ausgeführt:
// Warten
function waitForSomeTime(milliseconds) {
// asynchrone Wartezeit von Millisekunden Millisekunden
var task = $q.defer();
$timeout(function () {
task.resolve();
}, milliseconds);
// Die Aufgabe wird zurückgegeben
return task;
};
- Nach 3 Sekunden wird also Code ausgeführt. Dieser Code beendet die Aufgabe [task] mit einem Erfolgscode (resolve). Dies löst die Ausführung aller Codes aus, die auf diesen Abschluss gewartet haben (Zeile 4 unten):
// simuliertes Warten
task = utils.waitForSomeTime($scope.waiting.time);
// Wartezeit beendet
task.promise.then(function () {
// Erfolg
utils.debug('fin', new Date());
}, function () {
// Fehler
utils.debug('Opération annulée')
});
task.promise['finally'](function () {
// Wartezeitende in jedem Fall
$scope.waiting.visible = false;
});
- Die Zeile 6 oben (erfolgreicher Abschluss) wird also ausgeführt. Anschließend sind die Zeilen 11–14 an der Reihe. Sobald dieser Code ausgeführt ist, kehrt man zur Ansicht V zurück, die dann mit ihrem Modell M synchronisiert wird. Die Wartemeldung wird ausgeblendet (ng-show=$scope.waiting.visible=false, Zeile 13) und das Formular wird angezeigt (ng-hide=$scope.waiting.visible=false, Zeile 13);
Die Bildschirmanzeigen sehen dann wie folgt aus:
Wie oben zu sehen ist, beträgt die Wartezeit 3 Sekunden (06:01–05:58) zwischen Beginn und Ende der Wartephase. Wenn der Benutzer die Wartezeit hingegen vor Ablauf der 3 Sekunden abbricht, erscheint folgende Anzeige:
Abschließend ist es wichtig zu verstehen, dass es zu jedem Zeitpunkt nur einen Ausführungsthread gibt, der als UI-Thread (Benutzeroberfläche) bezeichnet wird. Das Ende einer asynchronen Aufgabe wird durch ein Ereignis signalisiert, genau wie der Klick auf eine Schaltfläche. Dieses Ereignis wird nicht sofort verarbeitet. Es wird in die Warteschlange der Ereignisse gestellt, die auf ihre Ausführung warten. Wenn es an der Reihe ist, wird es verarbeitet. Diese Verarbeitung erfolgt über den Thread von UI, und daher ist die Benutzeroberfläche während dieser Zeit eingefroren. Sie reagiert nicht auf Eingaben des Benutzers. Aus diesem Grund ist es wichtig, dass die Verarbeitung eines Ereignisses schnell erfolgt. Da jedes Ereignis vom Thread des UI verarbeitet wird, müssen niemals Synchronisationsprobleme zwischen gleichzeitig ausgeführten Threads gelöst werden. Zu jedem Zeitpunkt wird nur der Thread des UI ausgeführt.
3.7.6. Beispiel 6: Die Dienste HTTP
Wir stellen nun den Dienst [dao] vor, der mit dem Webserver kommuniziert:
![]() |
3.7.6.1. Die Ansicht V
![]() |
Wir werden ein Formular erstellen, um die Liste der Ärzte abzufragen:

Wir duplizieren [app-01.html] in [app-16.html] und ändern dieses anschließend wie folgt:
<div class="container" ng-cloak="">
<h1>Rdvmedecins - v1</h1>
<!-- die Warte-Meldung -->
<div class="alert alert-warning" ng-show="waiting.visible" ng-cloak="">
<h1>{{ waiting.text | translate}}
<button class="btn btn-primary pull-right" ng-click="waiting.cancel()">{{'msg_cancel'|translate}}</button>
<img src="assets/images/waiting.gif" alt=""/>
</h1>
</div>
<!-- die Anfrage -->
<div class="alert alert-info" ng-hide="waiting.visible">
<div class="form-group">
<label for="waitingTime">{{waitingTimeText | translate}}</label>
<input type="text" id="waitingTime" ng-model="waiting.time"/>
</div>
<div class="form-group">
<label for="urlServer">{{urlServerLabel | translate}}</label>
<input type="text" id="urlServer" ng-model="server.url"/>
</div>
<div class="form-group">
<label for="login">{{loginLabel | translate}}</label>
<input type="text" id="login" ng-model="server.login"/>
</div>
<div class="form-group">
<label for="password">{{passwordLabel | translate}}</label>
<input type="password" id="password" ng-model="server.password"/>
</div>
<button class="btn btn-primary" ng-click="execute()">{{medecins.title|translate:medecins.model}}</button>
</div>
<!-- die Liste der Ärzte -->
<div class="alert alert-success" ng-show="medecins.show">
{{medecins.title|translate:medecins.model}}
<ul>
<li ng-repeat="medecin in medecins.data">{{medecin.titre}}{{medecin.prenom}} {{medecin.nom}}</li>
</ul>
</div>
<!-- die Fehlerliste -->
<div class="alert alert-danger" ng-show="errors.show">
{{errors.title|translate:errors.model}}
<ul>
<li ng-repeat="message in errors.messages">{{message|translate}}</li>
</ul>
</div>
</div>
...
<script type="text/javascript" src="rdvmedecins-04.js"></script>
- Zeilen 13–31: Implementieren das Formular. Dieses ist nicht sichtbar, wenn die Wartemeldung angezeigt wird (ng-hide="waiting.visible"). Zu beachten ist, dass die vier Eingaben in (ng-model-Attribute) [waiting.time (ligne 16), server.url (ligne 20), server.login (ligne 24), server.password (ligne 28)] gespeichert werden;
- Zeilen 34–39: Zeigen die Liste der Ärzte an. Diese Liste ist nicht immer sichtbar (ng-show="medecins.show").
- Zeile 35: eine Alternative zur bereits bekannten Syntax <div ... translate="{{medecins.title}}" translate-values="{{medecins.model}}">;
- Zeile 36: eine ungeordnete Liste;
- Zeile 37: Die Liste der Ärzte befindet sich in der Vorlage [medecins.data]. Mit der Angular-Direktive [ng-repeat] lässt sich eine Liste durchlaufen. Die Syntax ng-repeat="medecin in medecins.data" bewirkt, dass das Tag <li> für jedes Element der Liste [medecins.data] wiederholt wird. Das aktuelle Element der Liste wird als [medecin] bezeichnet;
- Zeile 37: Für jedes <li> werden der Titel, der Vorname und der Nachname des aktuellen Arztes ausgegeben, der durch die Variable [medecin] bezeichnet wird;
- Zeilen 42–47: Zeigen die Fehlerliste an. Diese Liste ist nicht immer sichtbar (ng-show="errors.show"). Die Darstellung folgt dem gleichen Muster wie die Anzeige der Ärzte-Liste. Im Allgemeinen wird zur Anzeige einer Objektliste die Angular-Direktive [ng-repeat] verwendet;
- Zeile 51: Der JavaScript-Code befindet sich nun in der Datei [rdvmedecins-04]
3.7.6.2. Der Controller C und das Modell M
![]() |
Der JavaScript-Code entwickelt sich wie folgt weiter:

- Zeilen 6–9: Das Modul [rdvmedecins] deklariert eine Abhängigkeit vom Modul [base64], das von der Bibliothek [angular-base64] bereitgestellt wird, welche wiederum eine der Abhängigkeiten des Projekts ist. Dieses Modul dient dazu, die an den Webdienst zur Authentifizierung gesendete Zeichenkette „[login:password]“ in Base64 zu kodieren;
- Zeilen 12–13: Die Initialisierungsfunktion, die unsere internationalisierten Meldungen enthält. Es tauchen neue Meldungen auf. Wir werden diese nicht mehr einzeln vorstellen;
- Zeilen 69–70: der Dienst [config], der unsere Anwendung konfiguriert. Dort wurden neue Meldungsschlüssel hinzugefügt. Wir werden diese nicht mehr einzeln vorstellen;
- Zeilen 318–319: Der Dienst [utils], der Hilfsmethoden enthält. Dort werden neue hinzugefügt. Diese werden wir vorstellen;
- Zeilen 385–386: der Dienst [dao], der für den Austausch mit dem Webdienst zuständig ist. Auf ihn werden wir uns konzentrieren;
- Zeilen 467–468: der C-Controller der soeben vorgestellten Ansicht V. Wir werden ihn nun vorstellen, da er als „Dirigent“ fungiert und auf die Anfragen des Benutzers reagiert;
3.7.6.3. Der Controller C
Der Code des Controllers lautet wie folgt:
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate',
function ($scope, utils, config, dao, $translate) {
// ------------------- Initialisierung der Vorlage
// Vorlage
$scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: undefined};
$scope.waitingTimeText = config.waitingTimeText;
$scope.server = {url: undefined, login: undefined, password: undefined};
$scope.medecins = {title: config.listMedecins, show: false, model: {}};
$scope.errors = {show: false, model: {}};
$scope.urlServerLabel = config.urlServerLabel;
$scope.loginLabel = config.loginLabel;
$scope.passwordLabel = config.passwordLabel;
// asynchrone Aufgabe
var task;
// Aktion wird ausgeführt
$scope.execute = function () {
// UI wird aktualisiert
$scope.waiting.visible = true;
$scope.medecins.show = false;
$scope.errors.show = false;
// simulierte Wartezeit
task = utils.waitForSomeTime($scope.waiting.time);
var promise = task.promise;
// Wartezeit
promise = promise.then(function () {
// Anforderung der Ärzte-Liste;
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrMedecins);
return task.promise;
});
// das Ergebnis des vorherigen Aufrufs wird analysiert
promise.then(function (result) {
// result={err: 0, data: [med1, med2, ...]}
// Ergebnis={Fehler: n, Meldungen: [msg1, msg2, ...]}
if (result.err == 0) {
// Die erfassten Daten werden in das Modell eingefügt
$scope.medecins.data = result.data;
// man aktualisiert die UI
$scope.medecins.show = true;
$scope.waiting.visible = false;
} else {
// Beim Abrufen der Ärzte-Liste sind Fehler aufgetreten
$scope.errors = { title: config.getMedecinsErrors, messages: utils.getErrors(result), show: true, model: {}};
// Das UI wird aktualisiert
$scope.waiting.visible = false;
}
});
};
// Abbruch steht noch aus
function cancel() {
// Die Aufgabe wird abgeschlossen
task.reject();
// UI wird aktualisiert
$scope.waiting.visible = false;
$scope.medecins.show = false;
$scope.errors.show = false;
}
}
])
;
- Zeile 2: Der Controller hat eine neue Abhängigkeit, nämlich die vom Dienst [dao];
- Zeilen 6–13: Das Modell M der Ansicht V wird für deren erste Anzeige initialisiert;
- Zeile 8: [$scope.server] wird verwendet, um drei der vier Informationen aus dem Formular V abzurufen, wobei die vierte in [$scope.waiting.time] (Zeile 6) gespeichert ist;
- Zeile 9: [$scope.medecins] sammelt die für die Anzeige der Ärzte-Liste erforderlichen Informationen:
<!-- die Liste der Ärzte -->
<div class="alert alert-success" ng-show="medecins.show">
{{medecins.title|translate:medecins.model}}
<ul>
<li ng-repeat="medecin in medecins.data">{{medecin.titre}}{{medecin.prenom}} {{medecin.nom}}</li>
</ul>
</div>
Das Attribut [medecins.title] ist der Titel des Banners. Es ist im Dienst [config] definiert. Das Attribut [medecins.show] steuert, ob das Banner angezeigt wird oder nicht (Attribut ng-show="medecins.show"). Das Attribut [medecins.model] ist ein leeres Wörterbuch und wird es auch bleiben. Es dient lediglich dazu, die Verwendung der in Zeile 3 verwendeten Übersetzungsvariante zu veranschaulichen. Das Attribut [medecins.data], das die Liste der Ärzte enthalten wird (Zeile 5), ist noch nicht definiert.
- Zeile 10: [$scope.errors] sammelt die Informationen, die für die Anzeige der Fehlerliste erforderlich sind:
<!-- die Fehlerliste -->
<div class="alert alert-danger" ng-show="errors.show">
{{errors.title|translate:errors.model}}
<ul>
<li ng-repeat="message in errors.messages">{{message|translate}}</li>
</ul>
</div>
Das Attribut [errors.title] ist der Titel des Banners. Es wird im Dienst [config] definiert. Das Attribut [errors.show] steuert, ob das Banner angezeigt wird oder nicht (Attribut ng-show="errors.show"). Das Attribut [errors.model] ist ein leeres Wörterbuch und wird es auch bleiben. Es dient lediglich dazu, die Verwendung der in Zeile 3 verwendeten Übersetzungsvariante zu veranschaulichen. Das Attribut [errors.messages], das die Liste der anzuzeigenden Fehlermeldungen enthalten wird (Zeile 5), ist noch nicht definiert.
- Zeile 16: die asynchrone Aufgabe. Der Controller wird nacheinander zwei asynchrone Aufgaben starten. Die Verweise auf diese aufeinanderfolgenden Aufgaben werden in der Variablen [task] gespeichert. Dadurch können sie abgebrochen werden (Zeile 55);
- Zeile 19: Die Methode, die ausgeführt wird, wenn der Benutzer auf die Schaltfläche [Liste des médecins] klickt:
<button class="btn btn-primary" ng-click="execute()">Liste des médecins</button>
- Zeilen 21–23: Die Benutzeroberfläche wird aktualisiert: Die Wartemeldung wird angezeigt, alles andere wird ausgeblendet;
- Zeile 25: Die asynchrone Wartungsaufgabe wird erstellt. Nach Ablauf der vom Benutzer im Formular eingegebenen Zeit wird ein Signal (Aufgabe abgeschlossen) empfangen;
- Zeile 26: Das Promise der asynchronen Aufgabe wird abgerufen. Damit arbeitet das Programm, das die Aufgabe startet. Man benötigt jedoch die Referenz der Aufgabe selbst, um sie abbrechen zu können (Zeile 55);
- Zeilen 28–32: Hier wird definiert, welche Arbeit nach Beendigung der Wartezeit ausgeführt werden soll;
- Zeile 30: Mit der Methode [dao.getData] wird eine neue asynchrone Aufgabe gestartet. Dabei werden die erforderlichen Informationen übergeben:
- die Stamm-ID URL des Webdienstes [$scope.server.url], zum Beispiel [http://localhost:8080];
- das Login [$scope.server.login] zur Authentifizierung, zum Beispiel [admin];
- das Passwort [$scope.server.password] zur Authentifizierung, zum Beispiel [admin];
- die URL, die den angeforderten Dienst [config.urlSvrMedecins] bereitstellt, hier [/getAllMedecins]. Insgesamt lautet die vollständige URL dann [http://localhost:8080/getAllMedecins];
Die Methode [dao.getData] liefert ein Ergebnis, das zwei mögliche Formen haben kann:
- (Fortsetzung)
- {err: 0, data: [med1, med2, ...]}, wobei [medi] ein Objekt ist, das einen Arzt darstellt (Titel, Vorname, Nachname),
- {err: n, messages: [msg1, msg2, ...]}, wobei [msgi] eine Fehlermeldung ist und n ungleich 0 ist;
- Zeile 31: Das Versprechen der Aufgabe wird zurückgegeben. Hier gibt es etwas zu beachten. Es gibt zwei Versprechen:
- promise.then(): gibt ein erstes Promise [promise1] zurück;
- return task.promise: gibt ein zweites Promise [promise2] zurück;
- Letztendlich ist `promise=promise.then(...; return task.promise)` eine Kette aus zwei Promises `[promise2.promise1]`. [promise1] wird erst ausgewertet, wenn das Promise [promise2] erhalten wird, d. h. wenn die Aufgabe [dao.getData] abgeschlossen ist. Das Versprechen [promise1] hängt von keiner asynchronen Aufgabe ab. Es wird daher sofort erfüllt;
- Zeilen 34–50: Aus der vorstehenden Erläuterung folgt, dass diese Zeilen erst ausgeführt werden, wenn die Aufgabe [dao.getData] abgeschlossen ist. Der an die Funktion in Zeile 34 übergebene Parameter [result] wird durch die Methode [dao.getData] erstellt und durch die Operation [task.resolve(result)] an den aufrufenden Code übergeben, wobei [result] folgende Form hat:
- {err: 0, data: [med1, med2, ...]}, wobei [medi] ein Objekt ist, das einen Arzt darstellt (Titel, Vorname, Nachname),
- {err: n, messages: [msg1, msg2, ...]}, wobei [msgi] eine Fehlermeldung ist und n ungleich 0 ist;
- Zeile 37: Der Fehlercode [result.err] wird überprüft;
- Zeilen 38–42: Wenn kein Fehler vorliegt (result.err == 0), wird die Liste der Ärzte abgerufen und angezeigt;
- Zeilen 44–47: Liegt hingegen ein Fehler vor (result.err ≠ 0), wird die Liste der Fehlermeldungen abgerufen und angezeigt;
- Zeilen 53–56: Die Wartemeldung mit ihrer Abbruchschaltfläche bleibt so lange sichtbar, bis beide asynchronen Vorgänge abgeschlossen sind. Schauen wir uns an, was je nach Zeitpunkt des Abbruchs geschieht:
- Zunächst muss man verstehen, dass die Zeilen 19–50 in einem Durchgang ausgeführt werden. Es wurde also nur eine einzige asynchrone Aufgabe gestartet, nämlich die in Zeile 25.
- Nach dieser ersten Ausführung wird die Ansicht V aktualisiert, sodass die Warteanzeige und die Abbruchschaltfläche sichtbar sind. Wenn der Benutzer den Wartevorgang abbricht, bevor die Aufgabe in Zeile 25 abgeschlossen ist, wird die Methode in Zeile 53 ausgeführt und die Aufgabe mit einem Fehler abgebrochen (Zeile 55);
- Zeilen 56–59: Die Benutzeroberfläche wird aktualisiert: Das Formular wird erneut angezeigt und alles andere wird ausgeblendet,
- es erfolgt dann eine Rückkehr zur Ansicht V, und der Browser verarbeitet das nächste Ereignis. Da die Aufgabe beendet wurde, wird das Promise dieser Aufgabe abgerufen, wodurch ein Ereignis ausgelöst wird. Dieses wird anschließend verarbeitet;
- anschließend werden die Zeilen 28–32 ausgeführt. Es ist keine Funktion für den Fehlerfall definiert, daher wird kein Code ausgeführt. Man erhält ein neues Promise, das wie immer von [promise.then] zurückgegeben und wie immer abgerufen wird,
- da das Ereignis verarbeitet wurde, erfolgt eine Rückkehr zur Ansicht V, und der Browser verarbeitet das nächste Ereignis. Da das [promise] aus Zeile 28 verarbeitet wurde, wird das aus Zeile 34 aufgelöst, was ein neues Ereignis auslöst. Dieses wird dann verarbeitet;
- die Zeilen 34–49 werden nun der Reihe nach ausgeführt, da das in Zeile 34 verwendete Promise erfüllt wurde. Da erneut keine Funktion für den Fehlerfall definiert ist, wird kein Code ausgeführt,
- sodass wir zu Zeile 50 gelangen. Es besteht keine Task-Wartezeit mehr und die neue Ansicht V wird angezeigt;
- Nehmen wir nun an, dass die Abbruchaktion erfolgt, während die zweite asynchrone Aufgabe [dao.getData] gerade ausgeführt wird. Die vorstehende Argumentation gilt erneut. Das Ende der Aufgabe führt zur Ausführung der Zeilen 34–50 mit einem fehlgeschlagenen Aufgabenende. Wir werden bald feststellen, dass die Methode [dao.getData] einen asynchronen Aufruf von HTTP an den Webdienst durchführt. Dieser Aufruf wird nicht abgebrochen, aber sein Ergebnis wird nicht ausgewertet.
Es ist wichtig, dieses ständige Hin und Her zwischen der Anzeige der Ansicht V und der Verarbeitung von Browser-Ereignissen zu verstehen. Die Ereignisse werden entweder vom Benutzer (ein Klick) oder durch Systemvorgänge wie den Abschluss eines asynchronen Vorgangs ausgelöst. Der Ruhezustand des Browsers ist die Darstellung der Ansicht V. Er wird durch ein Ereignis aus diesem Ruhezustand herausgeholt, das dann vom Browser verarbeitet wird. Sobald das Ereignis verarbeitet wurde, kehrt er in seinen Ruhezustand zurück. Die Ansicht V wird dann aktualisiert, wenn das verarbeitete Ereignis ihr Modell M verändert hat. Der Browser wird durch das nächste Ereignis aus seinem Ruhezustand herausgeholt.
Alles läuft in einem einzigen Thread ab. Zwei Ereignisse werden niemals gleichzeitig verarbeitet. Ihre Ausführung erfolgt sequenziell. Der Browser wechselt erst zum nächsten Ereignis, wenn das vorherige ihm das Wort erteilt, in der Regel, weil es vollständig verarbeitet wurde.
Ein Punkt muss noch erläutert werden. Um Fehlermeldungen anzuzeigen, schreiben wir:
$scope.errors = { title: config.getMedecinsErrors, messages: utils.getErrors(result), show: true, model: {}};
Die Liste der Meldungen wird von der Methode [utils.getErrors] bereitgestellt, die im Service [utils] definiert ist. Diese Methode lautet wie folgt:
// Analyse der Fehler in der Serverantwort JSON
function getErrors(data) {
// Daten {err:n, messages:[]}, err!=0
// Fehler
var errors = [];
// Fehlercode
var err = data.err;
switch (err) {
case 2 :
// nicht autorisiert
errors.push('not_authorized');
break;
case 3 :
// Zugriff verweigert
errors.push('forbidden');
break;
case 4 :
// Lokaler Fehler
errors.push('not_http_error');
break;
case 6 :
// Dokument nicht gefunden
errors.push('not_found');
break;
default :
// Sonstige Fälle
errors = data.messages;
break;
}
// Wenn keine Meldung vorhanden ist, wird eine angezeigt
if (! errors || errors.length == 0) {
errors=['error_unknown'];
}
// Die Fehlerliste wird ausgegeben
return errors;
}
- Zeilen 2–3: Der empfangene Parameter [data] ist ein Objekt mit zwei Attributen:
- [err]: ein Fehlercode;
- [messages]: eine Liste von Meldungen;
- Zeile 5: Es wird ein Array mit Fehlermeldungen erstellt. Diese Meldungen sind internationalisiert. Aus diesem Grund werden nicht die Meldungen selbst in das Array aufgenommen, sondern ihre Internationalisierungsschlüssel – mit Ausnahme von Zeile 27. In diesem Fall wird das Attribut [messages] des Parameters [data] verwendet. Bei diesen Meldungen handelt es sich um echte Meldungen und nicht um Meldungsschlüssel. Die Sicht V behandelt sie jedoch als Meldungsschlüssel, die dann nicht gefunden werden. In diesem Fall zeigt das Modul [translate] den Meldungsschlüssel an, den es nicht gefunden hat, also hier eine echte Meldung. Dies ist das gewünschte Ergebnis;
- Zeilen 32–34: Behandeln den Fall, dass [data.messages] in Zeile 27 den Wert null hat. Dies tritt bei dem geschriebenen Webservice auf. Dieser Fall hätte vermieden werden müssen.
3.7.6.4. Der Dienst [dao]
![]() |
Der Dienst [dao] gewährleistet den Datenaustausch HTTP mit dem Webdienst / JSON. Sein Code lautet wie folgt:
angular.module("rdvmedecins")
.factory('dao', ['$http', '$q', 'config', '$base64', 'utils',
function ($http, $q, config, $base64, utils) {
// Protokolle
utils.debug("[dao] init");
// ----------------------------------private Methoden
// Daten vom Webdienst abrufen
function getData(serverUrl, username, password, urlAction, info) {
// asynchrone Operation
var task = $q.defer();
// URL-Anfrage HTTP
var url = serverUrl + urlAction;
// Basis-Authentifizierung
var basic = "Basic " + $base64.encode(username + ":" + password);
// die Antwort
var réponse;
// Alle HTTP-Anfragen müssen authentifiziert werden
var headers = $http.defaults.headers.common;
headers.Authorization = basic;
// Die Anfrage HTTP wird gestellt
var promise;
if (info) {
promise = $http.post(url, info, {timeout: config.timeout});
} else {
promise = $http.get(url, {timeout: config.timeout});
}
promise.then(success, failure);
// Die Aufgabe selbst wird zurückgegeben, damit sie abgebrochen werden kann
return task;
// Erfolg
function success(response) {
// response.data={status:0, data:[med1, med2, ...]} oder {status:x, data:[msg1, msg2, ...]
utils.debug("[dao] getData[" + urlAction + "] success réponse", response);
// Antwort
var payLoad = response.data;
réponse = payLoad.status == 0 ? {err: 0, data: payLoad.data} : {err: 1, messages: payLoad.data};
// die Antwort wird zurückgegeben
task.resolve(réponse);
}
// Fehler
function failure(response) {
utils.debug("[dao] getData[" + urlAction + "] error réponse", response);
// der Status wird analysiert
var status = response.status;
var error;
switch (status) {
case 401 :
// nicht autorisiert
error = 2;
break;
case 403:
// verboten
error = 3;
break;
case 404:
// nicht gefunden
error = 6;
break;
case 0:
// Lokaler Fehler
error = 4;
break;
default:
// Sonstiges
error = 5;
}
// die Antwort wird zurückgegeben
task.resolve({err: error, messages: [response.statusText]});
}
}
// --------------------- Instanz des Dienstes [dao]
return {
getData: getData
}
}]);
- Zeilen 77–79: Der Dienst hat nur ein einziges Feld: die Methode [getData], mit der Informationen vom Webdienst / JSON abgerufen werden können;
- Zeile 2: Hier taucht eine Abhängigkeit namens [$http] auf, auf die wir bisher noch nicht gestoßen sind. Es handelt sich um einen vordefinierten Angular-Dienst, der den Dialog HTTP mit einer entfernten Entität ermöglicht;
- Zeile 6: Ein Log, um zu sehen, zu welchem Zeitpunkt im Lebenszyklus der Anwendung der Code ausgeführt wird;
- Zeile 10: Die Methode [getData] akzeptiert fünf Parameter:
- [serverUrl]: die Stamm-URL des Webdienstes (http://localhost:8080);
- [urlAction]: die URL des angeforderten spezifischen Dienstes (/getAllMedecins);
- [username]: der Benutzername des Nutzers;
- [password]: sein Passwort;
- [info]: Objekt, das zusätzliche Informationen enthält, wenn der URL des angeforderten Dienstes über einen Vorgang POST angefordert wird. Im Fall von URL (/getAllMedecins) wurde dieser Parameter nicht übergeben. Er lautet daher [undefined];
- Zeile 12: Es wird eine asynchrone Aufgabe angelegt;
- Zeile 14: Der URL führt den angeforderten Dienst (http://localhost:8080/getAllMedecins) aus;
- Zeile 16: Die Authentifizierung erfolgt durch Senden des folgenden Headers HTTP:
wobei [code] der Base64-Code der Zeichenfolge [username:password] ist;
Zeile 16 bildet den Teil [Basic code] des Headers HTTP;
- Zeile 18: die Antwort des Webdienstes;
- Zeile 20: Die von Angular standardmäßig in einer Anfrage HTTP gesendeten Header HTTP sind im Objekt [$http.defaults.headers.common] definiert. Der Header [Authorization:Basic code] gehört nicht dazu;
- Zeile 21: Er wird zu den Headern HTTP hinzugefügt, die systematisch gesendet werden sollen. Links von der Zuweisung steht der zu initialisierende Header [Authorization] und rechts der Wert des Headers, in diesem Fall der in Zeile 16 definierte Wert. Wenn man also schreibt:
sendet Angular den Header HTTP:
- Zeile 23: Die Methoden des Dienstes [$http] geben Promises zurück. Diese werden in der Variablen [promise] gespeichert;
- Zeile 27: Da hier der Parameter [info] den Wert [undefined] hat, wird Zeile 27 ausgeführt. URL (http://localhost:8080/getAllMedecins) wird mit einem GET angefordert. Um nicht zu lange warten zu müssen, wird eine maximale Wartezeit (Timeout) für den Empfang der Serverantwort festgelegt. Standardmäßig beträgt diese eine Sekunde;
- Zeile 29: Hier werden die beiden Methoden definiert, die ausgeführt werden sollen, sobald das Promise erhalten wird:
- [success]: definiert in Zeile 34, ist die Methode, die ausgeführt werden soll, wenn das Promise bei erfolgreichem Abschluss der Aufgabe zurückgegeben wird;
- [failure]: definiert in Zeile 45, ist die Methode, die ausgeführt werden soll, wenn das Promise nach einem Fehlschlag der Aufgabe zurückgegeben wird;
- Beide Methoden (man sollte eigentlich von Funktionen sprechen) sind innerhalb der Funktion [getData] definiert. Dies ist in JavaScript möglich. Die in [getData] definierten Variablen sind in den beiden internen Funktionen [success, failure] bekannt;
- Zeile 31: Die in Zeile 12 erstellte Aufgabe wird zurückgegeben. Man muss sich hier an den aufrufenden Code erinnern:
promise = promise.then(function () {
// Liste der Ärzte wird angefordert;
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrMedecins);
return task.promise;
});
In Zeile 3 oben wird tatsächlich eine Aufgabe abgerufen.
- Zeile 34: Die Funktion [success] wird zu einem späteren Zeitpunkt ausgeführt, sobald der Aufruf von HTTP erfolgreich beendet ist. Dieser Begriff des Erfolgs bezieht sich auf die erste Zeile einer Antwort von HTTP. Diese hat folgende Form:
Der Code ist ein dreistelliger Text, der angibt, ob der Aufruf erfolgreich war oder nicht. Grob gesagt kann man sagen, dass die Codes 2xx und 3xx Erfolgscodes sind, während die anderen Fehlercodes sind. Der Text ist eine kurze Erläuterung. Hier sind zwei mögliche Antworten, eine im Erfolgsfall, die andere im Fehlerfall:
- Zeile 36: Die Antwort des Servers wird auf der Konsole angezeigt. Bei dem Fehler [404 Not Found] erhält man etwa Folgendes:
[dao] getData[/getAllMedecins] error réponse : {"data":"...","status":404,"config":{...},"statusText":"Not Found"}
In dieser Antwort verwenden wir nur die Felder [data], [status] und [statusText].
- Zeile 38: Wir extrahieren das Feld [data] aus der Antwort. Es hat eine der folgenden Formen:
- {status: 0, data: [med1, med2, ...]}, wobei [medi] ein Objekt ist, das einen Arzt repräsentiert (Titel, Vorname, Nachname),
- {status: n, data: [msg1, msg2, ...]}, wobei [msgi] eine Fehlermeldung ist und n ungleich 0 ist;
![]() |

- Zeile 39: Es wird die Antwort {0,data} oder {n,messages} erstellt. Die erste Antwort enthält die Ärzte im Feld [data]. Die zweite Antwort signalisiert einen Fehler, der serverseitig aufgetreten ist. Der Server hat diesen Fehler verarbeitet, einen Fehlercode in [err] und eine Liste von Fehlermeldungen in [data] generiert. In beiden Fällen wird ein Code HTTP 200 zurückgegeben, der angibt, dass der Auftrag HTTP vollständig bearbeitet wurde. Aus diesem Grund werden beide Fälle in derselben Funktion [success] verarbeitet;
- Zeile 41: Die Aufgabe ist abgeschlossen ([task.resolve]) und es wird eine der beiden Antworten zurückgegeben:
- {err: 0, data: [med1, med2, ...]}, wobei [medi] ein Objekt ist, das einen Arzt repräsentiert (Titel, Vorname, Nachname),
- {err: n, messages: [msg1, msg2, ...]}, wobei [msgi] eine Fehlermeldung ist und n ungleich 0 ist;
Dieser Code muss mit der Art und Weise verknüpft werden, wie diese Antwort im aufrufenden Code des Controllers abgerufen wird:
// das Ergebnis des vorherigen Aufrufs wird analysiert
promise.then(function (result) {
// result={err: 0, data: [med1, med2, ...]}
// Ergebnis={Fehler: n, Meldungen: [msg1, msg2, ...]}
...
}
Die Antwort von [task.resolve(réponse)] befindet sich oben in der Variablen [result].
- Zeile 45: Die Funktion [failure] wird aufgerufen, wenn die asynchrone Aufgabe fehlschlägt. Es gibt zwei mögliche Fälle:
- Der Server meldet diesen Fehler durch die Rückgabe eines Codes, der weder 2xx noch 3xx ist,
- Angular bricht den Aufruf HTTP ab. Es erfolgt dann kein Aufruf. Es liegt eine Angular-Ausnahme vor, aber der Server gibt keinen Fehlercode HTTP zurück. Dies ist beispielsweise der Fall, wenn ein ungültiger URL angegeben wird, der nicht aufgerufen werden kann;
- Zeile 46: Die Antwort wird in der Konsole angezeigt;
- Zeile 48: Wir erinnern uns daran, dass die Antwort des Servers folgende Form hat:
{"data":"...","status":404,"config":{...},"statusText":"Not Found"}
In Zeile 48 wird das oben genannte Attribut [status] abgerufen;
- Zeilen 50–70: Ausgehend vom Fehlercode HTTP wird ein neuer Fehlercode generiert, um den aufrufenden Codes die Art HTTP der Methode [dao.getData] zu verbergen. Man kann überprüfen, dass in dem Controller, der diese Methode verwendet, nichts darauf hindeutet, dass in der Methode ein Aufruf von HTTP enthalten ist;
- Zeile 51: Der Fehler [401] entspricht einem Fehlschlag der Authentifizierung (z. B. falsches Passwort),
- Zeile 55: Der Fehler [403] entspricht einem nicht autorisierten Aufruf. Der Benutzer hat sich zwar korrekt authentifiziert, verfügt jedoch nicht über ausreichende Berechtigungen, um die von ihm angeforderte URL abzurufen. Dies tritt beim Benutzer [user / user] auf. Dieser ist zwar in der Datenbank vorhanden, verfügt jedoch nicht über die Berechtigung, die Anwendung zu nutzen. Nur der Benutzer [admin / admin] hat diese Berechtigung;
- Zeile 59: Der Fehler [404] entspricht einem nicht gefundenen URL. Der Fehler kann mehrere Ursachen haben:
- Der Benutzer hat sich bei der Eingabe der URL des Dienstes vertippt;
- der Webdienst wurde nicht gestartet;
- der Webdienst hat nicht schnell genug geantwortet (Standardzeitlimit von einer Sekunde);
- Zeile 63: Der Fehlercode HTTP 0 existiert nicht. In diesem Fall hat Angular den angeforderten Aufruf HTTP nicht ausgeführt, da die vom Benutzer eingegebene URL ungültig ist und nicht aufgerufen werden kann. Im weiteren Verlauf werden wir auf weitere Fälle stoßen, in denen Angular den angeforderten Aufruf HTTP nicht ausführt;
- Zeile 72: Die Aufgabe wird erfolgreich abgeschlossen (task.resolve), indem eine Antwort vom Typ {err, messages} zurückgegeben wird, wobei das Array [messages] nur aus der einzigen Meldung [response.statusText] besteht. Falls Angular den angeforderten Aufruf HTTP nicht durchgeführt hat, erhalten wir eine leere Zeichenkette;
Da wir nun sowohl einen Gesamtüberblick als auch einen detaillierten Einblick in die Anwendung haben, können wir mit den Tests beginnen.
3.7.6.5. Tests der Anwendung – 1
Beginnen wir mit gültigen Eingaben:

![]() |
- Bei [1] geben wir 0 ein, um keine Wartezeit zu haben;
- bei [2] erscheint eine Fehlermeldung, obwohl die Eingaben korrekt sind. Wir haben die verschiedenen Fehlermeldungen nicht vorgestellt. Die bei [2] angezeigte Meldung ist eine allgemeine Fehlermeldung, die mit dem Fehler 0 verbunden ist, der einer Angular-Ausnahme entspricht. Angular ist auf ein Problem gestoßen, das den Aufruf von HTTP verhindert hat. In solchen Fällen muss man sich die Protokolle der JavaScript-Konsole ansehen. Dazu gibt es zwei Möglichkeiten:
- [F12] im Chrome-Browser ausführen;
- die WebStorm-Konsole verwenden;
In der WebStorm-Konsole finden wir verschiedene Meldungen, darunter diese:
- Zeile 1: Angular meldet einen Fehler, auf den wir noch zurückkommen werden;
- Zeile 2: Das Protokoll der Methode „[dao.getData]“. Darin finden wir einige interessante Informationen:
- [status] ist gleich 0, was darauf hinweist, dass kein Aufruf von HTTP stattgefunden hat. Folglich ist [statusText] leer,
- [url] entspricht [http://localhost:8080/getAllMedecins], was korrekt ist;
- der Header HTTP zur Authentifizierung von [Authorization":"Basic YWRtaW46YWRtaW4=] ist ebenfalls korrekt;
Na gut, warum hat es dann nicht funktioniert? Der entscheidende Eintrag in den Protokollen ist [No 'Access-Control-Allow-Origin' header is present]. Um ihn zu verstehen, bedarf es einer ausführlichen Erklärung. Beginnen wir damit, noch einmal auf die allgemeine Architektur der Client-Server-Anwendung einzugehen:

- Die Seiten HTML / CSS / JS der Angular-Anwendung stammen vom Server [1];
- In [2] sendet der Dienst [dao] eine Anfrage an einen anderen Server, nämlich den Server [2]. Nun, das wird vom Browser, der die Angular-Anwendung ausführt, unterbunden, da es sich um eine Sicherheitslücke handelt. Die Anwendung darf nur den Server abfragen, von dem sie stammt, d. h. den Server [1];
Tatsächlich ist es unzutreffend zu sagen, dass der Browser der Angular-Anwendung verbietet, den Server [2] abzufragen. Sie fragt ihn vielmehr ab, um zu erfahren, ob er einem Client, der nicht von ihm stammt, die Abfrage gestattet. Diese Technik wird als CORS (Cross-Origin Resource Sharing) bezeichnet. Der Server [2] erteilt seine Zustimmung, indem er bestimmte HTTP-Header sendet. Da unser Server [2] diese Header hier nicht gesendet hat, hat der Browser den von der Anwendung angeforderten Aufruf HTTP abgelehnt.
Gehen wir nun ins Detail. Betrachten wir den Netzwerkverkehr, der beim Aufruf von HTTP stattgefunden hat. Dazu drücken wir im Chrome-Browser die Tastenkombination [F12], um die Entwicklertools aufzurufen, und wählen den Reiter [Network] aus, um den Netzwerkverkehr anzuzeigen:
![]() |
- Bei [1] wählen wir den Reiter [network] aus;
- Bei [2] fordern wir die Liste der Ärzte an;
Auf der Registerkarte „[network]“ erhalten wir folgende Informationen:
![]() |
- in [1] die an den Server gesendeten Informationen;
- in [2] die Antwort des Servers;
In [1] ist zu sehen, dass der Browser eine Anfrage HTTP [OPTIONS] bezüglich der angeforderten URL gesendet hat. [OPTIONS] ist einer der möglichen Befehle, zu denen auch die bekannteren HTTP, [GET] und [POST] gehören. Mit ihr können Informationen von einem Server abgefragt werden, insbesondere zu den von ihm unterstützten HTTP-Optionen, daher auch der Name des Befehls. Der Server antwortet mit [2]. Um anzugeben, dass er Anfragen von Clients akzeptiert, die nicht zu seiner Domäne gehören, muss er einen speziellen Header namens [Access-Control-Allow-Origin] zurücksenden. Und gerade weil er diesen nicht zurückgesendet hat, hat Angular den angeforderten Aufruf HTTP nicht ausgeführt und den folgenden Fehler zurückgegeben:
XMLHttpRequest cannot load http://localhost:8080/getAllMedecins. Für die angeforderte Ressource ist kein „Access-Control-Allow-Origin“-Header vorhanden. Der Ursprung „http://localhost:63342“ hat daher keinen Zugriff.
Wir müssen daher unseren Server so anpassen, dass er den erwarteten Header „HTTP“ sendet.
3.7.6.6. Anpassung des Webservers / JSON
Wir kehren zu Eclipse zurück. Um die bisherigen Änderungen beizubehalten, duplizieren wir die aktuelle Version des Webservers / JSON [rdvmedecins-webapi-v2] in [rdvmedecins-webapi-v3] [1]:
![]() |
Wir nehmen eine erste Änderung in [ApplicationModel] vor, das eines der Konfigurationselemente des Webdienstes ist:
package rdvmedecins.web.models;
...
@Component
public class ApplicationModel implements IMetier {
// die Schicht [métier]
@Autowired
private IMetier métier;
// Daten aus der Schicht [métier]
private List<Medecin> médecins;
private List<Client> clients;
private List<String> messages;
// Konfigurationsdaten
private boolean CORSneeded = true;
...
public boolean isCORSneeded() {
return CORSneeded;
}
}
- Zeile 17: Wir definieren eine boolesche Variable, die angibt, ob Clients außerhalb der Serverdomäne akzeptiert werden oder nicht;
- Zeilen 21–23: die Methode für den Zugriff auf diese Information;
Anschließend erstellen wir einen neuen Spring-Controller MVC [3]:
![]() |
Die Klasse [RdvMedecinsCorsController] sieht wie folgt aus:
package rdvmedecins.web.controllers;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import rdvmedecins.web.models.ApplicationModel;
@Controller
public class RdvMedecinsCorsController {
@Autowired
private ApplicationModel application;
// Übermittlung der Optionen an den Client
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// Der Header wird festgelegt CORS
response.addHeader("Access-Control-Allow-Origin", "*");
}
}
// Ärzteverzeichnis
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.OPTIONS)
public void getAllMedecins(HttpServletResponse response) {
sendOptions(response);
}
}
- Zeilen 28–31: Definieren einen Controller für URL [/getAllMedecins], wenn dieser mit dem Befehl HTTP [OPTIONS] aufgerufen wird;
- Zeile 29: Die Methode [getAllMedecins] nimmt als Parameter das Objekt [HttpServletResponse] entgegen, das an den anfragenden Client gesendet wird. Dieses Objekt wird von Spring bereitgestellt;
- Zeile 30: Die Bearbeitung der Anfrage wird an die private Methode in den Zeilen 19–25 delegiert;
- Zeilen 15–16: Das Objekt [ApplicationModel] wird injiziert;
- Zeilen 20–23: Ist der Server so konfiguriert, dass er Clients außerhalb seiner Domäne akzeptiert, wird der Header HTTP gesendet:
Access-Control-Allow-Origin: *
was bedeutet, dass der Server Clients aus allen Domänen (*) akzeptiert.
Wir sind nun bereit für weitere Tests. Wir starten die neue Version des Webdienstes und stellen fest, dass das Problem weiterhin besteht. Es hat sich nichts geändert. Wenn wir in Zeile 30 oben eine Konsolenausgabe einfügen, wird diese nie angezeigt, was zeigt, dass die Methode [getAllMedecins] aus Zeile 29 nie aufgerufen wird.
Nach einigen Recherchen stellt sich heraus, dass Spring MVC die Befehle HTTP und [OPTIONS] selbst mit einer Standardverarbeitung bearbeitet. Daher antwortet immer Spring und niemals die Methode [getAllMedecins] in Zeile 29. Dieses Standardverhalten von Spring MVC lässt sich ändern. Wir fügen eine neue Konfigurationsklasse hinzu, um das neue Verhalten zu konfigurieren:
![]() |
Die neue Konfigurationsklasse [WebConfig] sieht wie folgt aus:
package rdvmedecins.web.config;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
// Konfiguration des Dispatcher-Servlets für die Header CORS
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet();
servlet.setDispatchOptionsRequest(true);
return servlet;
}
}
- Zeile 8: Die Klasse ist eine Spring-Konfigurationsklasse. Sie deklariert Beans, die in den Spring-Kontext aufgenommen werden;
- Zeile 12: Das Bean [dispatcherServlet] dient zur Definition des Servlets, das die Client-Anfragen verarbeitet. Es ist vom Typ [DispatcherServlet]. Dieses Servlet wird normalerweise standardmäßig erstellt. Wenn wir es selbst erstellen, können wir es anschließend konfigurieren;
- Zeile 14: Es wird eine Instanz vom Typ [DispatcherServlet] erstellt;
- Zeile 15: Es wird festgelegt, dass das Servlet die Befehle HTTP und [OPTIONS] an die Anwendung weiterleitet;
- Zeile 16: Das Servlet wird entsprechend konfiguriert;
Nun müssen wir noch die Klasse [AppConfig] ändern:
package rdvmedecins.web.config;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import rdvmedecins.config.DomainAndPersistenceConfig;
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class, SecurityConfig.class, WebConfig.class })
public class AppConfig {
}
- Zeile 11: Die neue Konfigurationsklasse [WebConfig] wird importiert;
3.7.6.7. Anwendungstests – 2
Wir starten die neue Version des Webdienstes / JSON und versuchen, mit unserem Angular-Client die Liste der Ärzte abzurufen. Wir überprüfen den Netzwerkverkehr auf der Registerkarte [Network]:
![]() |
- In [1] lässt sich feststellen, dass der Header HTTP [Access-Control-Allow-Origin: *] nun in der Antwort des Servers vorhanden ist. Und dennoch funktioniert es immer noch nicht. Wir überprüfen in [2] die Protokolle der Konsole. Dort finden wir das folgende Protokoll:
XMLHttpRequest cannot load http://localhost:8080/getAllMedecins. Das Request-Header-Feld „Authorization“ ist gemäß „Access-Control-Allow-Headers“ nicht zulässig
Man sieht, dass der Browser auf einen neuen Header „HTTP [Access-Control-Allow-Headers]“ wartet, der ihm mitteilen würde, dass wir berechtigt sind, ihm den Authentifizierungsheader zu senden:
Das könnte ein gutes Zeichen sein. Möglicherweise wollte Angular den Befehl HTTP GET senden. Da dieser jedoch mit einem Authentifizierungsheader einhergeht, fragt er nach, ob der Server diesen akzeptiert.
Wir passen unseren Webserver / JSON so an, dass er diesen Header sendet. Die Klasse [RdvMedecinsCorsController] wird wie folgt angepasst:
// Senden der Optionen an den Client
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// Der Header CORS wird gesetzt
response.addHeader("Access-Control-Allow-Origin", "*");
// Der Header wird zugelassen: [Authorization]
response.addHeader("Access-Control-Allow-Headers", "Authorization");
}
- In den Zeilen 6–7 wird der fehlende Header hinzugefügt.
Wir starten den Server neu und fordern die Liste der Ärzte erneut mit dem Angular-Client an:
![]() |
Diesmal klappt es. Die Konsolenprotokolle zeigen die von der Methode [dao.getData] empfangene Antwort:
[dao] getData[/getAllMedecins] success réponse : {"data":{"status":0,"data":[{"id":1,"version":1,"titre":"Mme","nom":"PELISSIER","prenom":"Marie"},{"id":2,"version":1,"titre":"Mr","nom":"BROMARD","prenom":"Jacques"},{"id":3,"version":1,"titre":"Mr","nom":"JANDOT","prenom":"Philippe"},{"id":4,"version":1,"titre":"Melle","nom":"JACQUEMOT","prenom":"Justine"}]},"status":200,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/getAllMedecins","headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4="}},"statusText":"OK"}
Wir sehen, dass:
- der Server einen Fehlercode [status=200] mit der Meldung [statusText=OK] zurückgegeben hat. Deshalb befinden wir uns in der Funktion [success];
- der Server hat ein Objekt [data] mit zwei Feldern zurückgegeben:
- [status]: (nicht zu verwechseln mit den Fehlercodes HTTP und [status]). Hier zeigt [status=0] an, dass URL und [/getAllMedecins] fehlerfrei verarbeitet wurden;
- [data]: enthält die Liste JSON der Ärzte;
Sehen wir uns nun weitere interessante Fälle an:
Es liegt ein Fehler bei den Kennungen [login, password] vor:
![]() |
Man meldet sich mit der Identität [user / user] an, die keinen Zugriff auf die Anwendung hat (nur [admin] hat Zugriff):
![]() |
Diesmal lautet der Fehler nicht mehr [Erreur d'authentification], sondern [Accès refusé].
3.7.7. Beispiel 7: Kundenliste
Wir greifen auf die vorherige Anwendung zurück, um diesmal die Kundenliste in einer Dropdown-Liste vom Typ [Bootstrap select] darzustellen (siehe Abschnitt 3.6.6).
3.7.7.1. Die Ansicht V
Die Ausgangsansicht sieht wie folgt aus:
![]() |
Um die Ansicht V zu erhalten, duplizieren wir den Code [app-16.html] in [app-17.html] und ändern ihn wie folgt:
<div class="container" >
<h1>Rdvmedecins - v1</h1>
<!-- Wartemeldung -->
<div class="alert alert-warning" ng-show="waiting.visible" >
...
</div>
<!-- die Anfrage -->
<div class="alert alert-info" ng-hide="waiting.visible" >
...
<button class="btn btn-primary" ng-click="execute()">{{clients.title|translate}}</button>
</div>
<!-- die Kundenliste -->
<div class="row" style="margin-top: 20px" ng-show="clients.show">
<div class="col-md-3">
<h2 translate="{{clients.title}}"></h2>
<select data-style="btn-primary" class="selectpicker">
<option ng-repeat="client in clients.data" value="{{client.id}}">
{{client.titre}} {{client.prenom}} {{client.nom}}
</option>
</select>
</div>
</div>
<!-- die Fehlerliste -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
</div>
....
<script type="text/javascript" src="rdvmedecins-05.js"></script>
- Zeilen 5–7: Die Warteanzeige bleibt unverändert;
- Zeilen 10–13: Das Formular bleibt unverändert, abgesehen von der Beschriftung der Schaltfläche (Zeile 12);
- Zeilen 28–30: Die Fehlermeldung bleibt unverändert;
- Zeilen 16–25: Die Anzeige der Kunden erfolgt in einer Dropdown-Liste, deren Stil durch die Komponente [Bootstrap-selectpicker] festgelegt wird (Attribute „data-style“, „class“, Zeile 19);
- Zeile 20: Die Direktive [ng-repeat] wird verwendet, um die verschiedenen Optionen der Dropdown-Liste zu generieren. Es ist zu beachten, dass die Bezeichnung einer Option vom Typ [Mme Julienne Tatou] ist und der Wert der Option vom Typ [100], wobei 100 die ID des angezeigten Kunden ist;
- Zeile 34: Der JavaScript-Code wird in eine neue Datei [rdvmedecins-05] ausgelagert;
3.7.7.2. Der Controller C und das Modell M
Der JavaScript-Code der Datei [rdvmedecins-05] wird durch Kopieren aus der Datei [rdvmedecins-04] übernommen:

Es ändert sich so gut wie nichts, außer im Controller, der nun so angepasst ist, dass er die Kundenliste bereitstellt:
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate',
function ($scope, utils, config, dao, $translate) {
// ------------------- Initialisierung der Vorlage
// Vorlage
$scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: undefined};
$scope.waitingTimeText = config.waitingTimeText;
$scope.server = {url: undefined, login: undefined, password: undefined};
$scope.clients = {title: config.listClients, show: false, model: {}};
$scope.errors = {show: false, model: {}};
$scope.urlServerLabel = config.urlServerLabel;
$scope.loginLabel = config.loginLabel;
$scope.passwordLabel = config.passwordLabel;
// asynchrone Aufgabe
var task;
// Aktion ausführen
$scope.execute = function () {
// UI wird aktualisiert
$scope.waiting.visible = true;
$scope.clients.show = false;
$scope.errors.show = false;
// simulierte Wartezeit
task = utils.waitForSomeTime($scope.waiting.time);
var promise = task.promise;
// Wartezeit
promise = promise.then(function () {
// Die Kundenliste wird angefordert;
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrClients);
return task.promise;
});
// das Ergebnis des vorherigen Aufrufs wird analysiert
promise.then(function (result) {
// result={err: 0, data: [client1, client2, ...]}
// Ergebnis={Fehler: n, Meldungen: [msg1, msg2, ...]}
if (result.err == 0) {
// Die erfassten Daten werden in das Modell eingefügt
$scope.clients.data = result.data;
// man aktualisiert das UI
$scope.clients.show = true;
$scope.waiting.visible = false;
// Die Dropdown-Liste wird gestaltet
$('.selectpicker').selectpicker();
} else {
// Beim Abrufen der Kundenliste sind Fehler aufgetreten
$scope.errors = { title: config.getClientsErrors, messages: utils.getErrors(result), show: true, model: {}};
// Das UI wird aktualisiert
$scope.waiting.visible = false;
}
});
};
// Abbruch steht noch aus
function cancel() {
// Die Aufgabe wird beendet
task.reject();
// UI wird aktualisiert
$scope.waiting.visible = false;
$scope.clients.show = false;
$scope.errors.show = false;
}
}
])
;
- Im Controller ändert sich nur sehr wenig. Er lieferte zuvor eine Liste von Ärzten. Nun liefert er eine Liste von Kunden;
- Zeile 9: [$scope.clients] wird das Template für die Kundenleiste in der Ansicht V sein;
- Zeile 30: Es wird nun URL [/getAllClients] verwendet;
- Zeilen 35–36: Die beiden Antwortformate, die von der Methode [dao.getData] zurückgegeben werden. Wir haben nun Kunden anstelle von Ärzten;
- Zeile 44: Eine im Angular-Code eher seltene Anweisung. Hier wird direkt auf das DOM (Document Object Model) zugegriffen. Hier soll die Methode [selectpicker] (Teil von [bootstrap-select.min.js]) auf die Elemente von DOM anwenden, die die Klasse [selectpicker] [$('.selectpicker')] haben. Es gibt nur eines davon, nämlich die Dropdown-Liste:
<select data-style="btn-primary" class="selectpicker" select-enable="">
....
</select>
In Abschnitt 3.6.6 wurde gezeigt, dass dies die Dropdown-Liste wie folgt formatiert:
![]() | ![]() |
Wie bereits bei den Ärzten müssen wir auch hier den Webdienst anpassen.
3.7.7.3. Änderung des Webdienstes – 1
![]() |
Die Klasse [RdvMedecinsController] wird um eine neue Methode erweitert:
package rdvmedecins.web.controllers;
...
@Controller
public class RdvMedecinsCorsController {
@Autowired
private ApplicationModel application;
// Optionen werden an den Kunden gesendet
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// Header wird festgelegt CORS
response.addHeader("Access-Control-Allow-Origin", "*");
// Der Header wird autorisiert: [Authorization]
response.addHeader("Access-Control-Allow-Headers", "Authorization");
}
}
// Liste der Ärzte
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.OPTIONS)
public void getAllMedecins(HttpServletResponse response) {
sendOptions(response);
}
// Liste der Kunden
@RequestMapping(value = "/getAllClients", method = RequestMethod.OPTIONS)
public void getAllClients(HttpServletResponse response) {
sendOptions(response);
}
}
- Zeilen 29–32: Die Methode [getAllClients] verarbeitet die Anfrage HTTP [OPTIONS], die ihr der Browser sendet;
3.7.7.4. Tests der Anwendung – 1
Wir sind nun bereit für einen Test. Wir starten den Webserver und geben dann gültige Werte in das Angular-Formular ein. Wir erhalten die folgende Antwort:

Diese Fehlermeldung wird angezeigt, wenn Angular die angeforderte Abfrage HTTP nicht ausführen konnte. Die Ursachen dafür müssen dann in den Konsolenprotokollen gesucht werden. Dort findet sich folgende Meldung:
XMLHttpRequest cannot load http://localhost:8080/getAllClients. Für die angeforderte Ressource ist kein „Access-Control-Allow-Origin“-Header vorhanden. Der Ursprung „http://localhost:63342“ hat daher keinen Zugriff.
Ein Problem, das man eigentlich für gelöst hielt. Sehen wir uns nun den stattgefundenen Netzwerkverkehr an:

Man sieht, dass der Vorgang [getAllClients] mit der Methode HTTP [OPTIONS]erfolgreich war, während der Vorgang [getAllClients] mit den Methoden HTTP und [GET] abgebrochen wurde. Die Antwort auf die Anfrage [OPTIONS] lautete wie folgt:

Die Kopfzeilen HTTP von CORS sind vorhanden. Sehen wir uns nun den Datenaustausch HTTP während GET an:

Die Anfrage HTTP scheint korrekt zu sein. Insbesondere ist der Authentifizierungsheader zu sehen.
Neben der vorherigen Fehlermeldung findet sich in den Konsolenprotokollen folgende Meldung:
[dao] getData[/getAllClients] error réponse : {"data":"","status":0,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/getAllClients","headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4="}},"statusText":""}
Dies ist das Protokoll, das die Methode [dao.getData] systematisch erstellt, sobald sie die Antwort auf ihre Anfrage HTTP erhält. Dabei fallen zwei Dinge auf:
- [status=0]: Das bedeutet, dass Angular die Anfrage HTTP abgebrochen hat;
- [method=GET]: Und es war die Anfrage GET, die abgebrochen wurde;
Zusammen mit der ersten Meldung bedeutet dies, dass Angular auch für die Anfrage GET die Header CORS erwartet. Derzeit sendet unser Webservice diese jedoch nur für die Anfragen HTTP und [OPTIONS]. Es ist sehr seltsam, dass dieser Fehler gerade jetzt auftritt und nicht bei der Ärzte-Liste. Ich habe keine Erklärung dafür.
Der Webdienst muss daher erneut angepasst werden.
3.7.7.5. Änderung des Webdienstes – 2
![]() |
Die Methoden [GET] und [POST] werden in der Klasse [RdvMedecinsController] verarbeitet. Wir müssen diese Klasse so ändern, dass diese Methoden die Header CORS senden. Das machen wir wie folgt:
@RestController
public class RdvMedecinsController {
@Autowired
private ApplicationModel application;
@Autowired
private RdvMedecinsCorsController rdvMedecinsCorsController;
...
// Kundenliste
@RequestMapping(value = "/getAllClients", method = RequestMethod.GET)
public Reponse getAllClients(HttpServletResponse response) {
// Kopfzeilen CORS
rdvMedecinsCorsController.getAllClients(response);
// Anwendungsstatus
if (messages != null) {
return new Reponse(-1, messages);
}
// Kundenliste
try {
return new Reponse(0, application.getAllClients());
} catch (Exception e) {
return new Reponse(1, Static.getErreursForException(e));
}
}
...
- Zeile 8: Wir möchten den Code wiederverwenden, den wir im Controller [RdvMedecinsCorsController] platziert haben. Daher fügen wir diesen hier ein;
- Zeile 14: Die Methode, die die Anfrage verarbeitet: [GET /getAllClients]. Wir nehmen zwei Änderungen vor:
- Zeile 14: Wir injizieren das Objekt [HttpServletResponse] in die Parameter der Methode,
- Zeile 16: Wir verwenden die Methoden der Klasse [RdvMedecinsCorsController], um die Header CORS in dieses Objekt einzufügen;
3.7.7.6. Anwendungstests – 2
Wir starten die neue Version des Webdienstes und fordern erneut die Kundenliste an. Wir erhalten folgende Antwort:
![]() |
- In [1] erhalten wir zwar eine Antwort, diese ist jedoch leer ([2]);
- bei [3]: Der Netzwerkverkehr verlief reibungslos;
In den Konsolenprotokollen hat die Methode [dao.getData] die empfangene Antwort angezeigt:
[dao] getData[/getAllClients] success réponse : {"data":{"status":0,"data":[{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"},{"id":2,"version":1,"titre":"Mme","nom":"GERMAN","prenom":"Christine"},{"id":3,"version":1,"titre":"Mr","nom":"JACQUARD","prenom":"Jules"},{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"}]},"status":200,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/getAllClients","headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4="}},"statusText":"OK"}
Die Methode hat also die Kundenliste tatsächlich erhalten. Nach Überprüfung des Codes fällt der Verdacht auf die folgende Anweisung, mit der wir nicht sehr vertraut sind:
// Gestaltung der Dropdown-Liste
$('.selectpicker').selectpicker();
Wir kommentieren Zeile 2 aus und versuchen es erneut. Daraufhin erhalten wir folgende Antwort:
![]() |
Wir haben das Problem also lokalisiert. Es ist die Anwendung der Methode [selectpicker] auf die Dropdown-Liste, die Probleme verursacht. Wenn wir uns den Quellcode der fehlerhaften Seite ansehen, finden wir Folgendes:
![]() |
- Man stellt fest, dass in [1] die Dropdown-Liste mit ihren Elementen zwar vorhanden ist, aber nicht angezeigt wird [style='display:none'];
- in [2] wird die Schaltfläche aus [bootstrap select] angezeigt. Die Elemente der Dropdown-Liste sollten in der Liste <ul role='menu'> erscheinen. Sie sind dort jedoch nicht vorhanden, sodass eine leere Liste angezeigt wird. Es scheint, dass der Inhalt der Dropdown-Liste zu dem Zeitpunkt, als die Methode [selectpicker] auf sie angewendet wurde, leer war;
Beim Stöbern im Internet auf der Suche nach einer Lösung stößt man auf diese hier. Man ersetzt den Code:
// Die Dropdown-Liste wird gestaltet
$('.selectpicker').selectpicker();
durch den folgenden:
// Die Dropdown-Liste wird gestaltet
$timeout(function(){
$('.selectpicker').selectpicker();
});
Der Stil [bootstrap-select] wird über die Funktion [$timeout] angewendet. Diese Funktion kennen wir bereits; sie ermöglicht es, eine Funktion nach Ablauf einer bestimmten Verzögerung auszuführen. Hier entspricht das Fehlen einer Verzögerung einer Verzögerung von Null. Die vorangegangenen Zeilen fügen ein Ereignis in die Warteschlange der Browser-Ereignisse ein. Sobald die Verarbeitung des aktuellen Ereignisses (Klick auf die Schaltfläche [Liste des clients]) abgeschlossen ist, wird die Ansicht V angezeigt. Unmittelbar danach überprüft der Browser seine Ereignisliste. Aufgrund der Verzögerung von Null steht das Ereignis [$timeout] an erster Stelle der Liste und wird verarbeitet. Der Stil [bootstrap-select] wird dann auf eine gefüllte Dropdown-Liste angewendet. Sehen wir uns das Ergebnis an:
![]() |
Wenn wir uns den Quellcode der angezeigten Seite noch einmal ansehen, finden wir Folgendes:
![]() |
Die Schaltfläche „[bootstrap-select]“, die zuvor leer war, enthält nun die Kundenliste.
3.7.7.7. Verwendung einer Direktive
Im Controller C der Ansicht V sind wir auf folgenden Code gestoßen:
// Die Dropdown-Liste wird gestaltet
$('.selectpicker').selectpicker();
Hier wird ein Objekt von DOM bearbeitet. Viele Angular-Entwickler scheuen sich davor, DOM im Code eines Controllers zu bearbeiten. Ihrer Meinung nach sollte dies in einer Direktive erfolgen. Eine Angular-Direktive kann als Erweiterung der Sprache HTML betrachtet werden. So ist es möglich, neue HTML-Elemente oder -Attribute zu erstellen. Sehen wir uns ein erstes Beispiel an:
Wir erstellen die folgende JS-Datei:
angular.module("rdvmedecins").directive('selectEnable', ['$timeout', function ($timeout) {
return {
link: function (scope, element, attrs) {
$timeout(function () {
var selectpicker = $('.selectpicker');
selectpicker.selectpicker();
});
}
};
}]);
- Die Anweisung folgt der uns mittlerweile vertrauten Syntax des Controllers:
angular.module("rdvmedecins").directive('selectEnable', ['$timeout', function ($timeout)
Die Direktive gehört zum Modul [rvmedecins]. Es handelt sich um eine Funktion, die zwei Parameter akzeptiert:
- (Fortsetzung)
- Der erste ist der Name der Direktive [selectEnable];
- der zweite ist ein Array ['obj1','obj2',..., function(obj1, obj2,...)], wobei die [obj] die Objekte sind, die in die Funktion eingefügt werden sollen. Hier ist das einzige eingefügte Objekt das vordefinierte Objekt [$timeout];
- Die Funktion [directive] gibt ein Objekt zurück, das verschiedene Attribute haben kann. Hier ist das einzige Attribut das Attribut [link] (Zeile 3). Sein Wert ist hier eine Funktion, die drei Parameter akzeptiert:
- scope: die Vorlage der Ansicht, in der die Direktive verwendet wird;
- element: das Element der Ansicht, auf das sich die Direktive bezieht;
- attrs: die Attribute dieses Elements;
Nehmen wir ein Beispiel. Die Direktive [selectEnable] könnte im folgenden Kontext verwendet werden:
Im obigen Beispiel wendet das Attribut [select-enable] die Direktive [selectEnable] auf das Element HTML <div> an. Eine Direktive [doSomething] kann auf jedes beliebige Element HTML angewendet werden, indem man ihm das Attribut [do-something] hinzufügt. Es ist auf die unterschiedliche Schreibweise zwischen dem Namen der Direktive und dem zugehörigen Attribut zu achten. Die Schreibweise ändert sich von [camelCase] zu [camel-case].
Die Direktive [selectEnable] könnte ebenfalls wie folgt verwendet werden:
Hier wird die Direktive [doSomething] in Form eines Tags HTML <do-something> angewendet.
Kehren wir zur Schreibweise zurück
und zu den drei Parametern der Funktion [link] der Direktive [scope, element, attrs]:
- scope: ist die Vorlage der Ansicht, in der sich das <div> befindet;
- element: ist das <div> selbst;
- attrs: ist das Array der Attribute des <div>. Diese können verwendet werden, um Informationen an die Direktive zu übermitteln. Im obigen Beispiel schreibt man attrs['selectEnable'], um die Information [data] zu erhalten. Beachten Sie die geänderte Schreibweise [selectEnable] zur Bezeichnung des Attributs [select-enable];
Kehren wir zum Code der Direktive zurück:
angular.module("rdvmedecins").directive('selectEnable', ['$timeout', function ($timeout) {
return {
link: function (scope, element, attrs) {
$timeout(function () {
$('.selectpicker').selectpicker();
});
}
};
}]);
- Zeilen 14–16: Hier finden wir den Code wieder, den wir zuvor im Controller platziert hatten. Dieser wird ausgeführt, sobald die Direktive [select-enable] (in Form eines Elements oder Attributs) bei der Anzeige der Ansicht V auftritt.
Um diese Direktive zu implementieren, kopieren wir die Datei [app-17.html] in [app-17B.html] und ändern sie wie folgt:
<select data-style="btn-primary" class="selectpicker" select-enable="">
<option ng-repeat="client in clients.data" value="{{client.id}}">
{{client.titre}} {{client.prenom}} {{client.nom}}
</option>
</select>
- Zeile 1: Wir wenden die Direktive [selectEnable] auf das Element HTML [select] an. Da keine Informationen an die Direktive übergeben werden müssen, schreiben wir einfach [select-enable=""];
Außerdem ändern wir den Controller, indem wir die Datei JS [rdvmedecins-05.js] in [rdvmedecins-05B.js] duplizieren und in der Direktivendatei [app-17B.html] und die Direktivendatei [selectEnable.js] verweisen. Dieser letzte Punkt darf nicht vergessen werden. Fehlt die Direktivendatei, wird das Attribut [select-enable=""] nicht verarbeitet, Angular meldet jedoch keinen Fehler.
<script type="text/javascript" src="rdvmedecins-05B.js"></script>
<script type="text/javascript" src="selectEnable.js"></script>
In der Datei JS [rdvmedecins-05B.js] entfernen wir aus dem Controller die folgenden Zeilen:
// Die Dropdown-Liste wird gestaltet
$timeout(function(){
$('.selectpicker').selectpicker();
});
da dieser Vorgang nun von der Direktive übernommen wird.
3.7.7.8. Tests der Anwendung – 3
Beim Testen der neuen Anwendung [app-17B.html] erhält man folgendes Ergebnis:
![]() |
- Bei [1] erhält man eine leere Liste.
Die Konsolenprotokolle zeigen Folgendes an:
- Zeile 1: Initialisierung des Dienstes [dao];
- Zeile 2: Bei der ersten Anzeige der Ansicht V wird die Anweisung [selectEnable] ausgeführt;
- Zeile 3: Diese Zeile erscheint, wenn der Benutzer auf die Schaltfläche [Liste des clients] klickt. Man stellt fest, dass die Anweisung [selectEnable] nicht ein zweites Mal ausgeführt wird. Letztendlich wurde sie ausgeführt, als die Kundenliste leer war, und daher haben wir eine leere Dropdown-Liste;
Anders ausgedrückt: „ “ den Vorgang:
$('.selectpicker').selectpicker();
nicht zum richtigen Zeitpunkt ausgeführt wurde. Man kann versuchen, das Problem auf verschiedene Weise zu lösen. Nach zahlreichen erfolglosen Tests stellt man fest, dass der oben genannte Vorgang nur einmal und ausschließlich dann ausgeführt werden darf, wenn die Dropdown-Liste bereits gefüllt ist. Um dieses Ergebnis zu erzielen, schreibt man das Tag <select> wie folgt um:
<select data-style="btn-primary" class="selectpicker" select-enable="" ng-if="clients.data">
<option ng-repeat="client in clients.data" value="{{client.id}}">
{{client.titre}} {{client.prenom}} {{client.nom}}
</option>
</select>
Zeile 1: Das Tag <select> wird nur generiert, wenn [clients.data] existiert. Dies ist bei der ersten Anzeige der Ansicht V nicht der Fall. Das Tag <select> wird daher nicht generiert und die Anweisung [selectEnable] nicht ausgewertet. Wenn der Benutzer auf die Schaltfläche [Liste des clients] klickt, erhält [clients.data] im Modell M einen neuen Wert. Da sich das Modell M geändert hat, wird das Tag <select> neu ausgewertet und nun generiert. Die Anweisung [selectEnable] wird daher ebenfalls ausgewertet. Zum Zeitpunkt ihrer Auswertung sind die Zeilen 2–4 des Tags <select> noch nicht ausgewertet worden. Wir haben also eine leere Kundenliste. Wenn wir die Anweisung [selectEnable] wie folgt schreiben:
angular.module("rdvmedecins").directive('selectEnable', ['$timeout', 'utils', function ($timeout, utils) {
return {
link: function (scope, element, attrs) {
utils.debug("directive selectEnable");
$('.selectpicker').selectpicker();
}
}
}]);
wird Zeile 5 mit einer leeren Liste ausgeführt, und es wird eine leere Dropdown-Liste angezeigt. Man muss daher schreiben:
angular.module("rdvmedecins").directive('selectEnable', ['$timeout', 'utils', function ($timeout, utils) {
return {
link: function (scope, element, attrs) {
utils.debug("directive selectEnable");
$timeout(function () {
$('.selectpicker').selectpicker();
})
}
}
}]);
um das erwartete Ergebnis zu erhalten. Aufgrund der Anweisung [$timeout] in Zeile 5 wird Zeile 6 erst nach vollständiger Auswertung der Ansicht V ausgeführt, also zu einem Zeitpunkt, zu dem das Tag <select> alle seine Elemente enthält.
3.7.8. Beispiel 8: Der Terminkalender eines Arztes
Wir stellen nun eine Anwendung vor, die den Terminkalender eines Arztes anzeigt.
3.7.8.1. Die Ansicht V der Anwendung
Wir stellen das folgende Formular vor:
![]() |
- In [1] wird der Terminkalender von Frau PELISSIER [2] für den 25. Juni 2014 [3] abgefragt;
Man erhält das folgende Ergebnis [4]:
![]() |
Wir werden die beiden Ansichten getrennt betrachten.
3.7.8.2. Das Formular
Wir duplizieren die Datei [app-17.html] in [app-18.html] und ändern den Code dann wie folgt:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- die Wartemeldung -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- die Anfrage -->
<div class="alert alert-info" ng-hide="waiting.visible">
<div class="row" style="margin-bottom: 20px">
<div class="col-md-3">
<h2 translate="{{medecins.title}}"></h2>
<select data-style="btn-primary" class="selectpicker">
<option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
{{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}
</option>
</select>
</div>
<div class="col-md-3">
<h2 translate="{{calendar.title}}"></h2>
<div style="display:inline-block; min-height:290px;">
<datepicker ng-model="calendar.jour" min-date="calendar.minDate" show-weeks="true"
class="well well-sm"></datepicker>
</div>
</div>
</div>
<button class="btn btn-primary" ng-click="execute()">{{agenda.title|translate}}</button>
</div>
<!-- die Fehlerliste -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- der Kalender -->
<div id="agenda" ng-show="agenda.show">
...
</div>
</div>
...
<script type="text/javascript" src="rdvmedecins-06.js"></script>
- Zeilen 5–7: Die Wartemeldung ändert sich nicht;
- Zeilen 12–19: die Ärzte-Liste vom Typ [bootstrap select];
- Zeilen 20–26: der Kalender von [ui-bootstrap], den wir bereits vorgestellt haben. Es ist zu beachten, dass der ausgewählte Tag in das Modell [calendar.jour] (Attribut ng-model) eingefügt wird;
- Zeile 28: die Schaltfläche zum Aufrufen des Kalenders;
- Zeilen 32–34: Die Fehlerliste ändert sich nicht;
- Zeilen 37–39: der Kalender, den wir später vorstellen werden;
- Zeile 42: Der Code JS wird durch Kopieren der Datei [rdvmedecins-05.js] in die Datei [rdvmedecins-06.js] übertragen;
3.7.8.3. Der Controller C
Der Code JS der Anwendung sieht nun wie folgt aus:

Von den Änderungen sind lediglich der Dienst [utils] und der Controller [rdvMedecinsCtrl] betroffen.
Der Controller [rdvMedecinsCtrl] lautet nun wie folgt:
// Controller
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate', '$timeout', '$filter', '$locale',
function ($scope, utils, config, dao, $translate, $timeout, $filter, $locale) {
// ------------------- Initialisierung der Vorlage
// Vorlage
$scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: 3000};
$scope.server = {url: 'http://localhost:8080', login: 'admin', password: 'admin'};
$scope.errors = {show: false, model: {}};
$scope.medecins = {
data: [
{id: 1, version: 1, titre: "Mme", nom: "PELISSIER", prenom: "Marie"},
{id: 2, version: 1, titre: "Mr", nom: "BROMARD", prenom: "Jacques"},
{id: 3, version: 1, titre: "Mr", nom: "JANDOT", prenom: "Philippe"},
{id: 4, version: 1, titre: "Melle", nom: "JACQUEMOT", prenom: "Justine"}
],
title: config.listMedecins};
$scope.agenda = {title: config.getAgendaTitle, data: undefined, show: false};
$scope.calendar = {title: config.getCalendarTitle, minDate: new Date(), jour: new Date()};
// Gestaltung der Dropdown-Liste
$timeout(function () {
$('.selectpicker').selectpicker();
});
// Französische Ländereinstellung für den Kalender
angular.copy(config.locales['fr'], $locale);
...
}
])
;
- Zeile 7: Es wird eine Wartezeit von 3 Sekunden festgelegt, bevor der Aufruf von HTTP erfolgt;
- Zeile 8: Die für die Verbindung mit HTTP erforderlichen Elemente werden fest vorgegeben;
- Zeilen 10–17: Die Liste der Ärzte wird fest definiert;
- Zeile 18: Die Vorlage [agenda] konfiguriert die Anzeige des Kalenders in der Ansicht;
- Zeile 19: Die Vorlage [calendar] konfiguriert die Anzeige des Kalenders in der Ansicht. Das Mindestdatum [minDate] wird auf heute festgelegt, ebenso wie das aktuelle Datum;
- Zeilen 21–23: Die Dropdown-Liste wird mit der zuvor beschriebenen Methode gestaltet;
- Zeile 25: Die Ländereinstellung der Anwendung wird auf „fr“ gesetzt. Standardmäßig ist sie auf „en“ eingestellt;
Die Methode, die bei der Abfrage des Kalenders ausgeführt wird, lautet wie folgt:
// Aktion ausführen
$scope.execute = function () {
// Formularinformationen
var idMedecin = $('.selectpicker').selectpicker('val');
// Überprüfung
utils.debug("[homeCtrl] idMedecin", idMedecin);
utils.debug("[homeCtrl] jour", $scope.calendar.jour);
// Das Datum wird im Format yyyy-MM-dd dargestellt
var formattedJour = $filter('date')($scope.calendar.jour, 'yyyy-MM-dd');
// Aktualisierung der Ansicht
$scope.waiting.visible = true;
$scope.errors.show = false;
$scope.agenda.show = false;
...
};
- Zeile 4: Das Attribut [value] des ausgewählten Arztes wird abgerufen. Hier wird erneut die Methode [selectpicker] verwendet, die aus der Datei [bootstrap-select.min.js] stammt. Man muss sich die Form der Optionen der Dropdown-Liste merken:
<option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
{{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}
Der Wert (Attribut value) der Option ist also die Kennung [id] des Arztes.
- Zeile 11: Der vom Benutzer ausgewählte Tag wird in das Format [aaaa-mm-jj] umgewandelt, das dem vom Webserver erwarteten Datumsformat entspricht;
- Zeilen 13–15: Sobald die Methode [execute] abgeschlossen ist, wird die Ladebalkenanzeige eingeblendet und alles andere ausgeblendet;
Der Code setzt sich wie folgt fort:
// Simulierte Wartezeit
var task = utils.waitForSomeTime($scope.waiting.time);
// Anfrage des Terminkalenders des Arztes
var promise = task.promise.then(function () {
// Der Pfad zum Dienst-URL
var path = config.urlSvrAgenda + "/" + idMedecin + "/" + formattedJour;
// Terminplan wird angefordert
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path);
// Rückgabe der Zusage zum Abschluss der Aufgabe
return task.promise;
});
// Das Ergebnis des Aufrufs des Dienstes [dao] wird analysiert
promise.then(function (result) {
// Wartezeit beendet
$scope.waiting.visible = false;
// Fehler?
if (result.err == 0) {
// Die Vorlage für den Kalender wird vorbereitet
$scope.agenda.data = result.data;
$scope.agenda.show = true;
// Formatierung der Zeitanzeige
angular.forEach($scope.agenda.data.creneauxMedecin, function (creneauMedecin) {
creneauMedecin.creneau.text = utils.getTextForCreneau(creneauMedecin.creneau);
});
// Es wird ein Ereignis erstellt, um die Tabelle nach der Anzeige der Ansicht zu formatieren
$timeout(function () {
$("#creneaux").footable();
});
} else {
// Es sind Fehler beim Abrufen des Kalenders aufgetreten
$scope.errors = {
title: config.getAgendaErrors,
messages: utils.getErrors(result),
show: true
};
}
- Zeile 2: die asynchrone Warteaufgabe von 3 Sekunden;
- Zeilen 5–10: Der Code, der ausgeführt wird, sobald diese Wartezeit abgelaufen ist;
- Zeile 6: Die abgefragte URL wird erstellt ([/getAgendaMedecinJour/1/2014-06-25]);
- Zeile 8: Das URL wird abgefragt. Eine asynchrone Aufgabe wird gestartet;
- Zeile 10: Das Promise dieser asynchronen Aufgabe wird zurückgegeben;
- Zeilen 14–38: Der Code, der ausgeführt wird, sobald der Aufruf von HTTP seine Antwort zurückgegeben hat;
- Zeile 13: [result] ist die von der Methode [dao.getData] gesendete Antwort. Dabei ist die Form der Antwort des Webservers zu beachten:
![]() |
Der Parameter [result.data] in Zeile 19 ist das oben genannte Attribut [data] [1]. Dieses Attribut enthält wiederum das oben genannte Attribut [creneauxMedecin] [2]. Dabei handelt es sich um ein Array von Zeitfenstern, wobei jedes Zeitfenster die folgenden beiden Informationen enthält:
- [rv]: die Form JSON eines Termins oder [null], falls in diesem Zeitfenster kein Termin vereinbart wurde;
- [hDeb, mDeb, hFin, mFin]: die Zeitangaben des Zeitfensters;
Kehren wir zum Code des Controllers zurück:
- Zeile 15: Die Wartezeit ist beendet;
- Zeile 19: Das Modell [$scope.agenda] wird mit Daten versorgt, das die Anzeige des Terminkalenders steuert;
- Zeile 20: Der Kalender wird sichtbar gemacht;
- Zeilen 22–24: Wir durchlaufen nacheinander jedes der C-Elemente des soeben erwähnten Arrays [creneauxMedecin];
- Zeile 23: Jedes C-Element verfügt über ein Attribut [creneau], das den Zeitblock angibt. Dieses wird um ein Attribut [text] erweitert, das die textuelle Darstellung des Zeitblocks in der Form [10h20:10h40] enthält;
- Zeilen 26–28: Die Tabelle HTML, die zur Anzeige der Zeitfenster im Kalender verwendet wird, wird „responsive“ gestaltet. Dieses Konzept haben wir in Abschnitt 3.6.7 behandelt;
![]() |
- Zeile 27: Um die Tabelle „responsive“ zu gestalten, muss die Methode [footable] auf sie angewendet werden. Hier tritt dieselbe Schwierigkeit auf wie bei der Komponente [bootstrap-select]. Wenn man einfach Zeile 17 schreibt, stellt man fest, dass die Tabelle nicht „responsive“ ist. Dieses Problem lässt sich auf die gleiche Weise mit der Funktion [$timeout] (Zeile 26) lösen;
- Zeilen 31–34: Der Fall, in dem der Aufruf von HTTP fehlgeschlagen ist. In diesem Fall werden die Fehlermeldungen angezeigt;
3.7.8.4. Anzeige des Kalenders
Wir kehren nun zum Code des Kalenders in der Datei [app-18.html] zurück. Dieser lautet wie folgt:
<!-- Der Kalender -->
<div id="agenda" ng-show="agenda.show">
<!-- Fall eines Arztes ohne Sprechstundentermine -->
<h4 class="alert alert-danger" ng-if="agenda.data.creneauxMedecin.length==0"
translate="agenda_medecinsanscreneaux"></h4>
<!-- Terminplan des Arztes -->
<div class="row tab-content alert alert-warning" ng-if="agenda.data.creneauxMedecin.length!=0">
<div class="tab-pane active col-md-6">
<table creneaux-table id="creneaux" class="table">
<thead>
<tr>
<th data-toggle="true">
<span translate="agenda_creneauhoraire"></span>
</th>
<th>
<span translate="agenda_client">Client</span>
</th>
<th data-hide="phone">
<span translate="agenda_action">Action</span>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="creneauMedecin in agenda.data.creneauxMedecin">
<td>
<span
ng-class="! creneauMedecin.rv ? 'status-metro status-active' : 'status-metro status-suspended'">
{{creneauMedecin.creneau.text}}
</span>
</td>
<td>
<span>{{creneauMedecin.rv.client.titre}} {{creneauMedecin.rv.client.prenom}} {{creneauMedecin.rv.client.nom}}</span>
</td>
<td>
<a href="" ng-if="!creneauMedecin.rv" translate="agenda_reserver" class="status-metro status-active">
</a>
<a href="" ng-if="creneauMedecin.rv" translate="agenda_supprimer" class="status-metro status-suspended">
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
- Zeilen 4–5: Wir erinnern uns, dass [agenda.data] der Kalender ist und dass [agenda.data.creneauxMedecin] ein Array von Objekten vom Typ [creneauMedecin] ist. Jedes Element des letztgenannten Typs verfügt über ein Attribut [creneauMedecin.creneau], das ein Zeitfenster darstellt. Jedes Zeitfenster enthält zwei Elemente, die für uns von Interesse sind:
- [creneauMedecin.creneau.rv], das den eventuellen RV (rv!=null) des Zeitfensters darstellt;
- [creneauMedecin.creneau.text], das dem Text [début:fin] des Zeitfensters entspricht;
- Zeile 4: Zeigt eine spezielle Meldung an, wenn der Arzt keine Zeitfenster hat. Das ist zwar unwahrscheinlich, aber da unsere Datenbank unvollständig ist, kommt dieser Fall tatsächlich vor. Ob die Meldung HTML generiert wird oder nicht, wird durch die Anweisung [ng-if] gesteuert;

Die Anweisung [ng-if] unterscheidet sich von den Anweisungen [ng-show, ng-hide]. Letztere beschränken sich darauf, ein im Dokument vorhandenes Feld auszublenden. Bei [ng-if='false'] wird das Feld hingegen aus dem Dokument entfernt. Wir haben es hier zur Veranschaulichung verwendet;
- Zeile 9: Das Attribut [id='creneaux'] ist wichtig. Es wird in der Anweisung verwendet:
$("#creneaux").footable();
- Zeilen 10–22: Zeigen die Spaltenüberschriften der Tabelle [1] an;
- Zeilen 23–45: zeigen den Inhalt der Tabelle [2] an;
![]() |
- Zeile 24: Das Array [agenda.data.creneauxMedecin] wird durchlaufen;
- Zeilen 26–29: Der Text [3] wird geschrieben. Dabei wird die Anweisung [ng-class] verwendet, die das Attribut [class] des Elements generiert. Wenn hier [creneauMedecin.rv==null] vorliegt, bedeutet dies, dass der Termin frei ist, und der Text erhält einen grünen Hintergrund. Andernfalls wird ein roter Hintergrund verwendet;
- Zeile 32: Hier wird der Name des Kunden geschrieben, für den der RV [4] gebucht wurde. Bei [rv==null] liegen diese Informationen nicht vor, aber Angular behandelt diesen Fall korrekt und meldet keinen Fehler;
- Zeilen 34–39: Zeigen eine der beiden Schaltflächen „[Réserver]“ oder „[Supprimer]“ an. Ob ein Termin vorliegt oder nicht, entscheidet darüber, welche der beiden Schaltflächen ausgewählt wird;
3.7.8.5. Änderung am Webserver
Wie bei den vorherigen Beispielen muss der Webserver so angepasst werden, dass URL [/getAgendaMedecinJour] die Header CORS sendet:
![]() |
In der Klasse [RdvMedecinsCorsController] wird eine neue Methode hinzugefügt:
// Terminplan des Arztes
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.OPTIONS)
public void getAgendaMedecinJour(HttpServletResponse response) {
sendOptions(response);
}
Diese Methode sendet die Header CORS für die Abfragen HTTP und [OPTIONS]. Das Gleiche muss für die Abfragen HTTP und [GET] in der Klasse [RdvMedecinsController] erfolgen:
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
public Reponse getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour, HttpServletResponse response) {
// Kopfzeilen CORS
rdvMedecinsCorsController.getAgendaMedecinJour(response);
...
}
3.7.8.6. Verwendung von Direktiven
Wie bereits zuvor werden wir die Bearbeitung von DOM in Direktiven auslagern. Wir haben zwei Bearbeitungen von DOM:
- bei der ersten Anzeige der Ansicht:
// Die Dropdown-Liste wird gestylt
$timeout(function () {
$('.selectpicker').selectpicker();
});
- bei der Anzeige des Kalenders:
// Es wird ein Ereignis erstellt, um die Tabelle nach dem Anzeigen der Ansicht zu gestalten
$timeout(function () {
$("#creneaux").footable();
});
Für den ersten Fall verwenden wir die bereits vorgestellte Direktive [selectEnable]. Für den zweiten Fall erstellen wir die Direktive [footable] in der folgenden Datei JS [footable.js]:
angular.module("rdvmedecins").directive('footable', ['$timeout', 'utils', function ($timeout, utils) {
return {
link: function (scope, element, attrs) {
utils.debug("directive footable");
$timeout(function () {
$("#creneaux").footable();
})
}
}
}]);
Wir wenden also dieselbe Technik an wie bei der Direktive [selectEnable].
Der Code HTML [app-18.html] wird in [app-18B.html] dupliziert. Anschließend wird er wie folgt weiterentwickelt:
<select data-style="btn-primary" class="selectpicker" select-enable="">
<option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
{{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}
</option>
</select>
- Zeile 1: Die Anweisung [selectEnable] (über das Attribut [select-enable]) wird auf das Tag <select> der Ärzte angewendet;
<div class="row tab-content alert alert-warning" ng-if="agenda.data.creneauxMedecin.length!=0">
<div class="tab-pane active col-md-6">
<table id="creneaux" class="table" footable="">
<thead>
<tr>
- Zeile 3: Die Direktive [footable] (über das Attribut [footable]) wird auf die Tabelle HTML des Terminkalenders angewendet;
<script type="text/javascript" src="rdvmedecins-06B.js"></script>
<!-- Richtlinien -->
<script type="text/javascript" src="selectEnable.js"></script>
<script type="text/javascript" src="footable.js"></script>
- Zeilen 3–4: Es wird auf die Dateien JS der beiden Anweisungen verwiesen;
- Zeile 1: Der Code JS aus [app-18B.html] ist der Code JS aus [app-18.html], der in der Datei [rdvmedecins-06B.js] dupliziert wurde;
Die Datei [rdvmedecins-06B.js] ist bis auf zwei Details identisch mit der Datei [rdvmedecins-06.js]. Die Zeilen, die sich auf DOM beziehen, verschwinden:
// Die Dropdown-Liste wird formatiert
$timeout(function () {
$('.selectpicker').selectpicker();
});
// Es wird ein Ereignis erstellt, um die Tabelle nach dem Anzeigen der Ansicht zu formatieren
$timeout(function () {
$("#creneaux").footable();
});
Somit liefert die Ausführung der Anwendung [app-18B.html] dieselben Ergebnisse wie die von [app-18.html].
3.7.9. Beispiel 9: Reservierungen anlegen und stornieren
Wir stellen nun eine Anwendung vor, mit der Reservierungen angelegt und storniert werden können.
3.7.9.1. Die Ansicht V der Anwendung
Wir stellen das folgende Formular vor:
![]() |
- In [1] können Reservierungen vorgenommen werden. Die Reservierung erfolgt für einen zufälligen Kunden;
- in [2] können die von uns vorgenommenen Reservierungen gelöscht werden;
Wir duplizieren die Datei [app-18.html] in [app-19.html] und ändern dann den Code wie folgt:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- die Wartemeldung -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- die Fehlerliste -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- der Terminkalender -->
<div id="agenda" ng-show="agenda.show">
..
<!-- Terminkalender des Arztes -->
<div class="row tab-content alert alert-warning" ng-if="agenda.data.creneauxMedecin.length!=0">
<div class="tab-pane active col-md-6">
<table id="creneaux" class="table" footable="">
...
<tbody>
<tr ng-repeat="creneauMedecin in agenda.data.creneauxMedecin">
...
<td>
<a href="" ng-if="!creneauMedecin.rv" translate="agenda_reserver" class="status-metro status-active" ng-click="reserver(creneauMedecin.creneau.id)">
</a>
<a href="" ng-if="creneauMedecin.rv" translate="agenda_supprimer" class="status-metro status-suspended" ng-click="supprimer(creneauMedecin.rv.id)">
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
....
<script type="text/javascript" src="rdvmedecins-07.js"></script>
<script type="text/javascript" src="footable.js"></script>
- Zeilen 5–7: Die Wartemeldung entspricht der der vorherigen Version;
- Zeilen 10–12: Die Wartemeldung entspricht der der vorherigen Version;
- Zeilen 15–36: Der Kalender entspricht dem der vorherigen Version, bis auf zwei Details:
- Zeile 26: Der Klick auf die Schaltfläche [réserver] (Attribut ng-click) wird von der Methode [reserver] des Modells M der Ansicht V verarbeitet. Dabei wird die Nummer des Reservierungszeitfensters übergeben;
- Zeile 26: Der Klick auf die Schaltfläche „[supprimer]“ wird von der Methode „[reserver]“ des Modells M der Ansicht V verarbeitet. Als Parameter wird die Nummer des zu löschenden Termins übergeben;
- Zeile 39: Der Code JS, der die Anwendung steuert, befindet sich in der Datei [rdvmedecins-07.js];
- Zeile 40: Der Code JS der in Zeile 20 angewendeten Direktive [footable];
3.7.9.2. Der Controller C
Der Code JS von [rdvmedecins-07.js] wird zunächst durch Kopieren aus der Datei [rdvmedecins-06.js] gewonnen. Anschließend wird er geändert. Es gibt weiterhin die üblichen großen Codeblöcke. Die Änderungen erfolgen im Wesentlichen im Controller:

Wir werden den Controller C der Ansicht V in mehreren Schritten beschreiben.
3.7.9.3. Initialisierung des C-Controllers
Der Initialisierungscode des Controllers lautet wie folgt:
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate', '$timeout', '$filter', '$locale',
function ($scope, utils, config, dao, $translate, $timeout, $filter, $locale) {
// ------------------- Initialisierung der Vorlage
// Vorlage
$scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: 3000};
$scope.server = {url: 'http://localhost:8080', login: 'admin', password: 'admin'};
$scope.errors = {show: false, model: {}};
$scope.medecins = {
data: [
{id: 1, version: 1, titre: "Mme", nom: "PELISSIER", prenom: "Marie"},
{id: 2, version: 1, titre: "Mr", nom: "BROMARD", prenom: "Jacques"},
{id: 3, version: 1, titre: "Mr", nom: "JANDOT", prenom: "Philippe"},
{id: 4, version: 1, titre: "Melle", nom: "JACQUEMOT", prenom: "Justine"}
],
title: config.listMedecins
};
var médecin = $scope.medecins.data[0];
var clients = [
{id: 1, version: 1, titre: "Mr", nom: "MARTIN", prenom: "Jules"},
{id: 2, version: 1, titre: "Mme", nom: "GERMAN", prenom: "Christine"},
{id: 3, version: 1, titre: "Mr", nom: "JACQUARD", prenom: "Maurice"},
{id: 4, version: 1, titre: "Melle", nom: "BISTROU", prenom: "Brigitte"}
];
// Französische Datumsformatierung
angular.copy(config.locales['fr'], $locale);
var today = new Date();
var formattedDay = $filter('date')(today, 'yyyy-MM-dd');
var fullDay = $filter('date')(today, 'fullDate');
$scope.agenda = {title: config.agendaTitle, data: undefined, show: false, model: {titre: médecin.titre, prenom: médecin.prenom, nom: médecin.nom, jour: fullDay}};
// ---------------------------------------------------------------- Anfangskalender
// die globale asynchrone Aufgabe
var task;
// Der Kalender wird abgefragt
getAgenda();
// ------------------------------------------------------------------ Reservierung
$scope.reserver = function (creneauId) {
....
};
// ------------------------------------------------------------ Löschung RV
$scope.supprimer = function (idRv) {
...
};
// Abruf des Terminkalenders
function getAgenda() {
...
}
// Stornierung in der Warteschlange
function cancel() {
...
}
} ]);
- Zeile 6: Konfiguration der Wartezeit. Standardmäßig wird 3 Sekunden gewartet, bevor ein Aufruf erfolgt: HTTP;
- Zeile 7: Die für die Aufrufe von HTTP erforderlichen Informationen;
- Zeile 8: Konfiguration der Fehlermeldung;
- Zeilen 9–17: fest hinterlegte Ärzte;
- Zeile 18: ein privater Arzt. Für seine Sprechstunden werden Termine vereinbart;
- Zeilen 19–24: Stammkunden;
- Zeile 26: Es sollen französische Datumsangaben bearbeitet werden;
- Zeile 27: Die Termine werden auf das heutige Datum festgelegt;
- Zeile 28: Der Web-Buchungsdienst erwartet Datumsangaben im Format „JJJJ-MM-TT“;
- Zeile 29: das heutige Datum im Format [jeudi 26 juin 2014];
- Zeile 30: Konfiguration des Kalenders. Das Attribut „[model]“ enthält die Parameter der internationalisierten Meldung, die angezeigt werden soll:
agenda_title: "Agenda de {{titre}} {{prenom}} {{nom}} le {{jour}}"
- Zeile 35: Die globale Variable [task] repräsentiert zu einem bestimmten Zeitpunkt die gerade ausgeführte asynchrone Aufgabe;
- Zeile 37: Der ursprüngliche Kalender wird angefordert;
Das ist alles, was beim ersten Laden der Seite geschieht. Wenn alles gut läuft, zeigt die Ansicht den Tageskalender von Frau PELISSIER an.

3.7.9.4. Abruf des Terminkalenders
Der Kalender wird mit der folgenden Methode [getAgenda] abgerufen:
// Abruf des Terminkalenders
function getAgenda() {
// der Pfad zum Dienst „URL“
var path = config.urlSvrAgenda + "/" + médecin.id + "/" + formattedDay;
// Terminanfrage
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path);
// Wartemeldung
$scope.waiting.visible = true;
// Das Ergebnis des Aufrufs des Dienstes [dao] wird analysiert
task.promise.then(function (result) {
// Wartezeit beendet
$scope.waiting.visible = false;
// Fehler?
if (result.err == 0) {
// Vorbereitung der Terminplanvorlage
$scope.agenda.data = result.data;
$scope.agenda.show = true;
// Formatierung der Zeitanzeige
angular.forEach($scope.agenda.data.creneauxMedecin, function (creneauMedecin) {
creneauMedecin.creneau.text = utils.getTextForCreneau(creneauMedecin.creneau);
});
} else {
// Beim Abrufen des Terminkalenders sind Fehler aufgetreten
$scope.errors = {title: config.getAgendaErrors, messages: utils.getErrors(result), show: true};
}
});
}
Dieser Code wurde bereits in der vorherigen Anwendung behandelt. Es gibt zwei Änderungen:
- Es gibt keine simulierte Wartezeit vor dem Aufruf von HTTP;
- Zeile 4: Es werden der bei der Initialisierung des Controllers angelegte Arzt sowie der zuvor erstellte formatierte Tag verwendet;
Dieser Code wurde in eine Funktion ausgelagert, da er auch von den Funktionen [reserver] und [supprimer] verwendet wird.
3.7.9.5. Reservierung eines Zeitfensters
![]() | ![]() |
Es sei daran erinnert, dass die Kunden nach dem Zufallsprinzip ausgewählt werden.
Der Buchungscode lautet wie folgt:
$scope.reserver = function (creneauId) {
utils.debug("réservation du créneau", creneauId);
// Es wird ein RV mit einem zufälligen Kunden in dem durch [id] identifizierten Zeitfenster erstellt
var idClient = clients[Math.floor(Math.random() * clients.length)].id;
utils.debug("réservation du créneau pour le client", idClient);
// Simulierte Wartezeit
$scope.waiting.visible = true;
var task = utils.waitForSomeTime($scope.waiting.time);
// Das Zeitfenster wird hinzugefügt
var promise = task.promise.then(function () {
// Der Pfad zum Dienst URL
var path = config.urlSvrResaAdd;
// die an den Dienst zu übermittelnden Daten
var post = {jour: formattedDay, idCreneau: creneauId, idClient: idClient};
// Die asynchrone Aufgabe wird gestartet
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path, post);
// Das Versprechen für den Abschluss der Aufgabe wird zurückgegeben
return task.promise;
});
// Auswertung des Aufgabenergebnisses
promise = promise.then(function (result) {
if (result.err != 0) {
// Es sind Fehler bei der Validierung des RV aufgetreten
$scope.errors = {title: config.postResaErrors, messages: utils.getErrors(result, $filter), show: true};
} else {
// Der neue Zeitplan wird angefordert
getAgenda();
}
});
};
- Zeile 1: Es sei daran erinnert, dass der Parameter der Funktion [reserver] die Nummer des Zeitfensters ist (Attribut id);
- Zeile 4: Ein Kunde wird zufällig aus der im Initialisierungscode fest definierten Kundenliste ausgewählt. Von ihm wird die Kennung [id] übernommen;
- Zeilen 7–8: Wartezeit von 3 Sekunden;
- Zeilen 11–18: Diese Zeilen werden erst nach Ablauf der 3 Sekunden ausgeführt;
- Zeile 12: Die URL des Buchungsdienstes [/ajouterRv]. Diese URL unterscheidet sich von den bisher behandelten. Sie ist im Webdienst wie folgt definiert:
@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse ajouterRv(@RequestBody PostAjouterRv post, HttpServletResponse response) {
- (Fortsetzung)
- Zeile 1: Die URL hat keine Parameter und wird zusammen mit einer POST angefordert;
- Zeile 2: Die übermittelten Parameter liegen in Form eines JSON-Objekts vor. Dieses wird im Parameter [post] (@RequestBody) deserialisiert;
Ein Beispiel für dieses POST haben wir bereits gesehen (Abschnitt 2.12.2):
![]() |
- in [0], das URL des Webdienstes;
- in [1] wird die Methode POST verwendet;
- in [2] der Text JSON der an den Webdienst übermittelten Informationen in der Form {Tag, idClient, idCreneau};
- in [3] teilt der Client dem Webdienst mit, dass er ihm Informationen JSON sendet;
Kehren wir zum Code JS der Funktion [reserver] zurück:
- Zeile 14: Der zu übermittelnde Wert wird in Form eines Objekts JS erstellt. Angular wird ihn beim Senden in JSON serialisieren;
- Zeile 16: Der Aufruf von HTTP erfolgt. Der zu übermittelnde Wert ist der letzte Parameter der Funktion [dao.getData]. Wenn dieser Parameter vorhanden ist, führt die Funktion [dao.getData] einen Aufruf von POST anstelle von GET durch (siehe Code in Abschnitt 3.7.6.4);
- Zeile 18: Das Promise des Aufrufs von HTTP wird zurückgegeben;
- Zeilen 23–29: werden erst ausgeführt, wenn der Aufruf HTTP seine Antwort zurückgegeben hat;
- Zeile 23: Der Parameter [result] hat die Form [err,data] oder [err,messages], wobei [err] ein Fehlercode ist;
- Zeilen 23–26: Wenn Fehler aufgetreten sind, wird die Fehlermeldung angezeigt;
- Zeile 28: Wenn die Buchung erfolgreich war, wird der neue Kalender erneut angezeigt;
3.7.9.6. Serveränderung
![]() |
In der Klasse [RdvMedecinsCorsController] fügen wir die folgende Methode hinzu:
// Senden der Optionen an den Kunden
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// Der Header wird festgelegt: CORS
response.addHeader("Access-Control-Allow-Origin", "*");
// Der Header wird freigegeben [authorization]
response.addHeader("Access-Control-Allow-Headers", "authorization");
}
@RequestMapping(value = "/ajouterRv", method = RequestMethod.OPTIONS)
public void ajouterRv(HttpServletResponse response) {
sendOptions(response);
}
Die Ergänzung erfolgt in den Zeilen 10–13. Die Kopfzeilen der Zeilen 2–8 werden an die Methoden URL und [/ajouterRv] (Zeile 10) sowie an die Methoden HTTP und [OPTIONS] (Zeile 10) gesendet.
Die Klasse [RdvMedecinsController] wird wie folgt geändert:
@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse ajouterRv(@RequestBody PostAjouterRv post, HttpServletResponse response) {
// Header CORS
rdvMedecinsCorsController.ajouterRv(response);
...
Für die Methode [POST] (Zeile 1) und die Methoden URL sowie [/ajouterRv] (Zeile 1) wird die soeben in [RdvMedecinsCorsController] hinzugefügte Methode aufgerufen (Zeile 4), wodurch dieselben Kopfzeilen wie bei den Methoden HTTP und [OPTIONS] zurückgegeben werden.
3.7.9.7. Tests
Führen wir einen ersten Test durch, bei dem wir einen beliebigen Termin reservieren:
![]() |
Wie immer in solchen Fällen muss man sich die Konsolenprotokolle ansehen:
[dao] getData[/ajouterRv] error réponse : {"data":"","status":0,"config":{"method":"POST","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/ajouterRv","data":{"jour":"2014-06-30","idCreneau":1,"idClient":4},"headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4=","Content-Type":"application/json;charset=utf-8"}},"statusText":""}
Die Methode [dao.getData] ist mit [status=0] fehlgeschlagen, was bedeutet, dass Angular die Anfrage abgebrochen hat. Die Fehlerursache finden wir in den Protokollen:
XMLHttpRequest cannot load http://localhost:8080/ajouterRv. Das Request-Header-Feld „Content-Type“ ist gemäß „Access-Control-Allow-Headers“ nicht zulässig.
Betrachtet man den Netzwerkverkehr, so ergibt sich Folgendes:
![]() |
- in [1] und [2]: Es gab nur eine Anfrage HTTP, die Anfrage [OPTIONS];
- in [3] fordert der Angular-Client zwei Berechtigungen an:
- die Berechtigung zum Senden der Header HTTP und [accept, authorization, content-type];
- die Berechtigung zum Senden eines Befehls POST;
- in [4]: Der Server erteilt die Berechtigung für den Header [authorization]. Zur Erinnerung: Auf der Serverseite senden wir diese Berechtigung selbst;
Neu ist also, dass der Angular-Client bei einem Vorgang mit dem Befehl POST weitere Berechtigungen vom Server anfordert. Dieser muss daher so angepasst werden, dass er sie gewährt:
![]() |
In der Klasse [RdvMedecinsCorsController] ändern wir die private Methode, die die HTTP-Header generiert, die für die Befehle OPTIONS, GET und POST gesendet werden:
// Senden der Optionen an den Client
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// Der Header CORS wird festgelegt
response.addHeader("Access-Control-Allow-Origin", "*");
// Bestimmte Header werden zugelassen
response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
// POST wird zugelassen
response.addHeader("Access-Control-Allow-Methods", "POST");
}
}
- Zeile 7: Es wurde eine Berechtigung für die Header HTTP und [accept, content-type] hinzugefügt;
- Zeile 9: Es wurde eine Berechtigung für die Methode POST hinzugefügt;
Nach dem Neustart des Servers wird der Test erneut durchgeführt:
![]() |
Diesmal ist die Buchung gelungen.
3.7.9.8. Löschen eines Termins
![]() | ![]() |
Der Funktionscode [supprimer] lautet wie folgt:
$scope.supprimer = function (idRv) {
utils.debug("suppression rv n°", idRv);
// Simulierte Wartezeit
$scope.waiting.visible = true;
task = utils.waitForSomeTime($scope.waiting.time);
// Das Zeitfenster wird hinzugefügt
var promise = task.promise.then(function () {
// Der Pfad zum Dienst URL
var path = config.urlSvrResaRemove;
// die an den Dienst zu übertragenden Daten
var post = {idRv: idRv};
// Die asynchrone Aufgabe wird gestartet
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path, post);
// Das Versprechen für den Abschluss der Aufgabe wird zurückgegeben
return task.promise;
});
// Auswertung des Aufgabenergebnisses
promise = promise.then(function (result) {
if (result.err != 0) {
// Beim Löschen des RV sind Fehler aufgetreten
$scope.errors = {title: config.postRemoveErrors, messages: utils.getErrors(result, $filter), show: true};
// UI wird aktualisiert
$scope.waiting.visible = false;
} else {
// Der neue Terminkalender wird angefordert
getAgenda();
}
});
};
- Zeile 1: Man muss bedenken, dass der Parameter der Funktion die Nummer des zu löschenden Termins ist. Der Code ähnelt hier stark dem der Reservierung. Wir gehen nur auf die Unterschiede ein;
- Zeile 9: Die URL des Dienstes lautet hier [/supprimerRV] und wird auch hier über eine POST aufgerufen:
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse supprimerRv(@RequestBody PostSupprimerRv post, HttpServletResponse response) {
Der gesendete Parameter wird auch hier in der Form JSON übertragen. In Abschnitt 2.12.17 haben wir die Beschaffenheit des manuell erstellten POST gezeigt:
![]() |
- in [1], dem URL des Webdienstes;
- in [2] wird die Methode POST verwendet;
- in [3], der Text JSON der an den Webdienst in der Form {idRv} übermittelten Informationen;
- in [4] teilt der Client dem Webdienst mit, dass er ihm Informationen JSON sendet;
Kehren wir zum Code JS der Funktion [supprimer] zurück:
- Zeile 11: Das Post-Objekt wird erstellt. Angular serialisiert es automatisch in JSON;
Der Rest des Codes entspricht dem der Reservierung.
3.7.9.9. Änderungen auf dem Server
Auf der Serverseite nehmen wir folgende Änderungen vor:
![]() |
In der Klasse [RdvMedecinsCorsController] fügen wir die folgende Methode hinzu:
// Optionen werden an den Kunden gesendet
private void sendOptions(HttpServletResponse response) {
if (application.isCORSneeded()) {
// Der Header wird festgelegt CORS
response.addHeader("Access-Control-Allow-Origin", "*");
// Bestimmte Header werden zugelassen
response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
// POST wird zugelassen
response.addHeader("Access-Control-Allow-Methods", "POST");
}
}
...
@RequestMapping(value = "/supprimerRv", method = RequestMethod.OPTIONS)
public void supprimerRv(HttpServletResponse response) {
sendOptions(response);
}
Die Ergänzung erfolgt in den Zeilen 13–16. Die Header der Zeilen 2–10 werden an die Methoden URL und [/supprimerRv] (Zeile 13) sowie an die Methoden HTTP und [OPTIONS] (Zeile 13) gesendet.
Die Klasse [RdvMedecinsController] wird wie folgt geändert:
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse supprimerRv(@RequestBody PostSupprimerRv post, HttpServletResponse response) {
// Header CORS
rdvMedecinsCorsController.supprimerRv(response);
...
Für die Methode [POST] (Zeile 1) und die Methoden URL sowie [/supprimerRv] (Zeile 1) wird die soeben hinzugefügte Methode [RdvMedecinsCorsController] aufgerufen (Zeile 4), wodurch dieselben Kopfzeilen wie bei den Methoden HTTP und [OPTIONS] zurückgegeben werden.
3.7.10. Beispiel 10: Reservierungen erstellen und stornieren – 2
Wir stellen nun dieselbe Anwendung wie zuvor vor, doch anstatt eine Reservierung für einen zufälligen Kunden vorzunehmen, wird dieser aus einer Dropdown-Liste ausgewählt.
3.7.10.1. Die Ansicht V der Anwendung
Wir stellen das folgende Formular vor:
![]() |
Die Kunden werden in [1] ausgewählt.
Der Code ähnelt dem der vorherigen Anwendung, daher stellen wir nur die wichtigsten Unterschiede vor.
Wir duplizieren die Datei [app-19.html] in [app-20.html] und erstellen anschließend den Code für die Dropdown-Liste der Kunden [1]:
<!-- die Kundenliste -->
<div class="alert alert-info">
<h3>{{agenda.title|translate:agenda.model}}</h3>
<div class="row" ng-show="clients.show">
<div class="col-md-3">
<h2 translate="{{clients.title}}"></h2>
<select data-style="btn-primary" class="selectpicker" select-enable="" ng-if="clients.data">
<option ng-repeat="client in clients.data" value="{{client.id}}">
{{client.titre}} {{client.prenom}} {{client.nom}}
</option>
</select>
</div>
</div>
</div>
- Zeilen 8–12: Die Dropdown-Liste wird mit der Komponente [bootstrap-select] implementiert;
- Zeile 1: Die Direktive [selectEnable] wird über das Attribut [select-enable] angewendet;
- Zeile 1: Das Tag <select> wird nur generiert, wenn [clients.data] vorhanden ist (# null, undefined). Dieser Punkt ist wichtig und wurde in Abschnitt 3.7.7.8 erläutert;
Außerdem importieren wir neue Dateien JS:
<script type="text/javascript" src="rdvmedecins-08.js"></script>
<!-- Richtlinien -->
<script type="text/javascript" src="selectEnable.js"></script>
<script type="text/javascript" src="footable.js"></script>
- Zeile 1: Die Datei [rdvmedecins-08.js] wird durch Kopieren der Datei [rdvmedecins-0.js] erstellt;
- Zeilen 3–4: Die Dateien der beiden Anweisungen werden importiert;
3.7.10.2. Der C-Controller
Der Code des C-Controllers entwickelt sich wie folgt:
// Controller
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate', '$timeout', '$filter', '$locale',
function ($scope, utils, config, dao, $translate, $timeout, $filter, $locale) {
// ------------------- Initialisierung der Vorlage
...
// die Kunden
$scope.clients = {title: config.listClients, show: false, model: {}};
//------------------------------------------- Initialisierung der Ansicht
// die globale asynchrone Aufgabe
var task;
// Zuerst werden die Kunden abgefragt, dann der Kalender
getClients().then(function () {
getAgenda();
});
...
// Aktion ausführen
function getClients() {
....
};
} ]);
- Zeile 8: Das Objekt „[$scope.clients]“ konfiguriert die Dropdown-Liste der Kunden in der Ansicht V;
- Zeilen 14–16: Asynchron wird zunächst die Kundenliste angefordert; sobald diese vorliegt, wird der Terminkalender von Frau PELISSIER für den heutigen Tag abgefragt. Die hier verwendete Syntax funktioniert nur, weil die Funktion [getClients] ein Promise zurückgibt;
Die Methode [getClients] fordert die Kundenliste an:
function getClients() {
// UI wird aktualisiert
$scope.waiting.visible = true;
$scope.clients.show = false;
$scope.errors.show = false;
// Die Kundenliste wird abgefragt;
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrClients);
var promise = task.promise;
// das Ergebnis des vorherigen Aufrufs wird analysiert
promise = promise.then(function (result) {
// result={err: 0, data: [client1, client2, ...]}
// Ergebnis={Fehler: n, Meldungen: [msg1, msg2, ...]}
if (result.err == 0) {
// Die erfassten Daten werden in das Modell eingefügt
$scope.clients.data = result.data;
// man aktualisiert die UI
$scope.clients.show = true;
$scope.waiting.visible = false;
} else {
// Beim Abrufen der Kundenliste sind Fehler aufgetreten
$scope.errors = { title: config.getClientsErrors, messages: utils.getErrors(result), show: true, model: {}};
// Das UI wird aktualisiert
$scope.waiting.visible = false;
}
});
// Das Versprechen wird erfüllt
return promise;
};
Dies ist ein Code, den wir bereits kennengelernt und kommentiert haben. Wichtig ist Zeile 31:
- Zeile 27: Hier wird das Promise aus Zeile 10 zurückgegeben, d. h. das zuletzt im Code erhaltene Promise. Dieses Promise wird erst dann erhalten, wenn der Aufruf von HTTP seine Antwort zurückgegeben hat;
Die Methode [reserver] hat sich leicht verändert:
$scope.reserver = function (creneauId) {
utils.debug("réservation du créneau", creneauId);
// Es wird ein RV für den ausgewählten Kunden erstellt
var idClient = $(".selectpicker").selectpicker('val');
...
});
- Zeile 4: Es wird nicht mehr für einen zufälligen Kunden reserviert, sondern für den in der Kundenliste ausgewählten Kunden.
3.7.11. Beispiel 11: Eine Anweisung [selectEnable2]
Dieses Beispiel befasst sich erneut mit den Anweisungen.
3.7.11.1. Die Ansicht V
Die Anwendung zeigt die folgende Ansicht an:
![]() |
3.7.11.2. Der Code HTML der Ansicht
Der Code HTML der Ansicht [app-21.html] lautet wie folgt:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- die Wartemeldung -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- die Fehlerliste -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- die Kundenliste -->
<div class="alert alert-info">
<div class="row" ng-show="clients.show">
<div class="col-md-4">
<h2 translate="{{clients.title}}"></h2>
<select data-style="btn-primary" id="selectpickerClients" select-enable2="" ng-if="clients.data">
<option ng-repeat="client in clients.data" value="{{client.id}}">
{{client.titre}} {{client.prenom}} {{client.nom}}
</option>
</select>
</div>
</div>
</div>
<!-- die Ärzte-Liste -->
<div class="alert alert-info">
<div class="row" ng-show="medecins.show">
<div class="col-md-4">
<h2 translate="{{medecins.title}}"></h2>
<select data-style="btn-primary" id="selectpickerMedecins" select-enable2="" ng-if="medecins.data">
<option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
{{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}
</option>
</select>
</div>
</div>
</div>
</div>
...
<script type="text/javascript" src="rdvmedecins-09.js"></script>
<!-- Anweisungen -->
<script type="text/javascript" src="selectEnable2.js"></script>
- Zeilen 19–23: die Dropdown-Liste der Kunden;
- Zeile 19: Die Anweisung [selectEnable2] (Attribut [select-enable2]) wird angewendet;
- Zeile 19: nur wenn [clients.data] nicht leer ist;
- Zeile 19: Die Dropdown-Liste wird durch das Attribut [id="selectpickerClients"] identifiziert;
- Zeilen 33–37: die Dropdown-Liste der Ärzte;
- Zeile 33: Die Anweisung [selectEnable2] (Attribut [select-enable2]) wird angewendet;
- Zeile 33: nur wenn [medecins.data] nicht leer ist;
- Zeile 33: Die Dropdown-Liste wird durch das Attribut [id="selectpickerMedecins"] identifiziert;
- Zeile 43: Es wird eine neue Datei JS [rdvmedecins-09.js] importiert;
- Zeile 45: Die Datei JS aus der neuen Anweisung wird importiert;
3.7.11.3. Die Direktive [selectEnable2]
Der Code der Direktive [selectEnable2] lautet wie folgt:
angular.module("rdvmedecins").directive('selectEnable2', ['$timeout', 'utils', function ($timeout, utils) {
return {
link: function (scope, element, attrs) {
utils.debug("directive selectEnable2 attrs", attrs);
$timeout(function () {
$('#' + attrs['id']).selectpicker();
})
}
}
}]);
- Zeile 4: Der Wert des Parameters [attrs] wird angezeigt, um die Funktionsweise des Codes zu verdeutlichen. Wir werden feststellen, dass attrs['id']='selectpickerClients' für die Kundenliste gilt;
- Zeile 6: Um in DOM ein Element von [id='x'] zu finden, schreibt man [$('#x')]. Man muss also [$('#selectpickerClients')] eingeben, um die Kundenliste zu finden. Dies wird mit der Syntax [$('#' + attrs['id'])] erreicht;
Die Anweisung [selectEnable2] nutzt also die Informationen, die von einem der Attribute des Elements HTML übertragen werden, auf das sie angewendet wird.
3.7.11.4. Der Controller C
Der C-Controller befindet sich in der Datei JS [rdvmedecins-09.js] und hat folgende Struktur:
// Controller
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao',
function ($scope, utils, config, dao) {
// ------------------- Initialisierung des Modells
// die Wartemeldung
$scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: 3000};
// Anmeldeinformationen
$scope.server = {url: 'http://localhost:8080', login: 'admin', password: 'admin'};
// Fehler
$scope.errors = {show: false, model: {}};
// die Ärzte
$scope.medecins = {title: config.listMedecins, show: false, model: {}};
// die Patienten
$scope.clients = {title: config.listClients, show: false, model: {}};
// die globale asynchrone Aufgabe
var task;
// ---------------------------------------------------- Initialisierung der Ansicht
// UI wird aktualisiert
$scope.waiting.visible = true;
$scope.clients.show = false;
$scope.medecins.show = false;
$scope.errors.show = false;
// Es werden zunächst die Kunden und anschließend die Ärzte abgefragt
getClients().then(function () {
getMedecins();
});
// Kundenliste
function getClients() {
...
}
// Ärzteverzeichnis
function getMedecins() {
...
}
// Stornierung ausstehend
function cancel() {
...
}
} ]);
- Zeilen 26–28: Zunächst werden die Kunden und anschließend die Ärzte abgefragt;
3.7.11.5. Tests
Testen Sie diese neue Version.
3.7.12. Beispiel 12: Eine Direktive [list]
Wir greifen das gleiche Beispiel wie zuvor auf, möchten aber den Code HTML mithilfe einer Direktive vereinfachen. Derzeit haben wir nämlich den folgenden Code HTML:
<!-- Kundenliste -->
<div class="alert alert-info">
<div class="row" ng-show="clients.show">
<div class="col-md-4">
<h2 translate="{{clients.title}}"></h2>
<select data-style="btn-primary" id="selectpickerClients" select-enable2="" ng-if="clients.data">
<option ng-repeat="client in clients.data" value="{{client.id}}">
{{client.titre}} {{client.prenom}} {{client.nom}}
</option>
</select>
</div>
</div>
</div>
<!-- Liste der Ärzte -->
<div class="alert alert-info">
<div class="row" ng-show="medecins.show">
<div class="col-md-4">
<h2 translate="{{medecins.title}}"></h2>
<select data-style="btn-primary" id="selectpickerMedecins" select-enable2="" ng-if="medecins.data">
<option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
{{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}
</option>
</select>
</div>
</div>
</div>
Die Zeilen 14–26 sind identisch mit den Zeilen 1–13. Sie beziehen sich auf Ärzte statt auf Kunden. Wir möchten Folgendes schreiben können:
<!-- Kundenliste -->
<list model="clients" ng-if="clients.show"></list>
<!-- Liste der Ärzte -->
<list model="medecins" ng-if="medecins.show"></list>
Dieser Code erfordert eine neue Direktive [list], die wir nun erstellen werden.
3.7.12.1. Die Direktive [list]
Die Direktive [list] wird in die Datei JS [list.js] eingefügt. Ihr Code lautet wie folgt:
angular.module("rdvmedecins")
.directive("list", ['utils', '$timeout', function (utils, $timeout) {
// Instanz der zurückgegebenen Direktive
return {
// Element HTML
restrict: "E",
// URL des Fragments
templateUrl: "list.html",
// für jede Instanz der Direktive eindeutiger Geltungsbereich
scope: true,
// Funktion zur Verknüpfung mit dem Dokument
link: function (scope, element, attrs) {
utils.debug("directive list attrs", attrs);
scope.model = scope[attrs['model']];
utils.debug("directive list model", scope.model);
$timeout(function () {
$('#' + scope.model.id).selectpicker();
})
}
}
}]);
- Zeile 2: definiert eine Direktive namens „list“;
- Zeile 6: Das Attribut [restrict] legt fest, wie die Direktive verwendet werden kann. [restrict: "E"] bedeutet, dass die Direktive [list] als Element HTML <list ...>...</list> verwendet werden kann. [restrict: "A"] bedeutet, dass die Direktive [list] als Attribut verwendet werden kann, zum Beispiel <div ... list='...'>. [restrict: "AE"] bedeutet, dass die Direktive [list] sowohl als Attribut als auch als Element verwendet werden kann;
- Zeile 8: Das Attribut [templateUrl] gibt den Namen des Fragments HTML an, das beim Auftreten des Tags verwendet werden soll. Dieses Fragment bildet den Hauptteil des Tags;
- Zeile 10: Das Attribut [scope] legt den Geltungsbereich der Vorlage der Direktive fest. [scope: true] bedeutet, dass zwei Elemente vom Typ <list> jeweils ihre eigene Vorlage haben. Standardmäßig (Geltungsbereich nicht initialisiert) teilen sie sich ihre Vorlagen;
- Zeile 12: Die Funktion [link], die wir bereits mehrfach verwendet haben;
Um den obigen Code zu verstehen, muss man sich die beabsichtigte Verwendung der Direktive vor Augen führen:
<!-- die Kundenliste -->
<list model="clients" ng-if="clients.show"></list>
<!-- die Liste der Ärzte -->
<list model="medecins" ng-if="medecins.show"></list>
Die Direktive [list] wird als Element HTML <list> verwendet. Dieses Element hat zwei Attribute:
- [model]: Sein Wert ist das Element des Modells M der Ansicht V, in der sich die Direktive [list] befindet. Dieses Element wird in das Modell der Direktive übernommen;
- [ng-if]: Dieses Attribut sorgt dafür, dass der Code HTML der Direktive nicht generiert wird, wenn nichts anzuzeigen ist;
Kehren wir zum Code der Funktion [link] der Direktive zurück:
link: function (scope, element, attrs) {
utils.debug("directive list attrs", attrs);
scope.model = scope[attrs['model']];
utils.debug("directive list model", scope.model);
$timeout(function () {
$('#' + scope.model.id).selectpicker();
})
}
Verbinden wir diesen Code JS mit dem Code HTML, der die Direktive verwendet:
<list model="clients" ng-if="clients.show"></list>
- Zeile 3: attrs['model'] hat hier den Wert „clients“;
- Zeile 3: scope[attrs['model']] hat den Wert scope['clients'] und steht somit für [$scope.clients], d. h. das Feld [clients] des Ansichtsmodells. Dieses Feld hat den Wert {id: '...', data: [client1, client2, ...], show: ..., title: '...'};
- Zeile 3: Ein Feld [model] wird zum Modell der Direktive hinzugefügt. Diese hat das Modell der Ansicht geerbt, in der sie sich befindet. Daher müssen Kollisionen mit einem möglichen Feld [model] vermieden werden, das die Ansicht ebenfalls haben könnte. Hier kommt es zu keiner Kollision;
- Zeile 4: [scope.model] wird angezeigt, um den Code besser zu verstehen;
- Zeilen 5–7: Hier taucht ein bereits bekannter Code auf. Der Unterschied besteht darin, dass das id der Komponente zuvor in einem Attribut attrs['id'] enthalten war. Hier wird es in [scope.model.id] enthalten sein;
Sehen wir uns nun den von der Direktive generierten Code HTML an. Aufgrund des Attributs [templateUrl: "list.html"] der Direktive muss man ihn in der Datei [list.html] suchen:
<!-- eine Liste von Kunden oder Ärzten -->
<div class="alert alert-info" ng-show="model.show">
<div class="row">
<div class="col-md-4">
<h2 translate="{{model.title}}"></h2>
<select data-style="btn-primary" id="{{model.id}}" ng-if="model.data">
<option ng-repeat="element in model.data" value="{{element.id}}">
{{element.titre}} {{element.prenom}} {{element.nom}}
</option>
</select>
</div>
</div>
</div>
- Das Erste, was man beim Lesen dieses Codes beachten muss, ist, dass die Direktive ein Objekt [scope.model] in der Form [{id :'...', data:[client1, client2, ...], show : ..., title :'...'}] erstellt hat. Dieses Objekt [model] (scope ist im Code HTML impliziert) wird vom Code HTML der Direktive verwendet;
- Zeile 2: Verwendung von [model.show] zum Ein- bzw. Ausblenden der von der Direktive generierten Ansicht;
- Zeile 5: Verwendung von [model.title], um einen Titel festzulegen;
- Zeile 6: Verwendung von [model.id], um dem Tag <select> eine ID zuzuweisen. Dieser id wird vom Code JS der Direktive verwendet;
- Zeile 6: Verwendung von [model.data], um das <select> nur dann zu generieren, wenn Daten angezeigt werden sollen;
- Zeilen 7–9: Verwendung von [model.data] zur Generierung der Elemente der Dropdown-Liste;
3.7.12.2. Der Code HTML
Der Code HTML der Anwendung [app-22.html] lautet wie folgt:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- die Wartemeldung -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- die Fehlerliste -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- die Kundenliste -->
<list model="clients" ng-if="clients.show"></list>
<!-- die Liste der Ärzte -->
<list model="medecins" ng-if="medecins.show"></list>
</div>
...
<script type="text/javascript" src="rdvmedecins-10.js"></script>
<!-- Anweisungen -->
<script type="text/javascript" src="list.js"></script>
- Zeile 22: Vergessen Sie nicht, den Code JS in die Anweisung aufzunehmen;
3.7.12.3. Der Controller C
Der C-Controller ändert sich kaum:
angular.module("rdvmedecins")
.controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao',
function ($scope, utils, config, dao) {
// ------------------- Initialisierung der Vorlage
...
// die Ärzte
$scope.medecins = {title: config.listMedecins, show: false, id: 'medecins'};
// die Kunden
$scope.clients = {title: config.listClients, show: false, id: 'clients'};
...
- In den Zeilen 7 und 9 fügen wir das Attribut [id] zu den Vorlagen für Ärzte und Kunden hinzu;
3.7.12.4. Die Tests
Die Tests liefern dieselben Ergebnisse wie im vorherigen Beispiel.
3.7.13. Beispiel 13: Aktualisierung der Vorlage einer Direktive
Wir bleiben bei der Untersuchung von Direktiven und behalten das Beispiel der Dropdown-Liste bei. Hier wollen wir das Verhalten der Direktive [list] untersuchen, wenn sich der Inhalt der Dropdown-Liste ändert.
3.7.13.1. Die Ansichten V
Es gibt folgende Ansichten:
![]() |
- In [1] wird zunächst die Kundenliste angefordert;
![]() |
- In [2] wird ein zweites Mal die Kundenliste angefordert. Diese zweite Liste wird dann mit der ersten ([3]) zusammengefasst. In diesem Beispiel soll die Aktualisierung der Komponente [Bootstrap select] untersucht werden.
3.7.13.2. Die Seite HTML
Die Seite HTML [app-23.html] wird durch Kopieren von [app-22.html] erstellt und anschließend wie folgt geändert:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- die Wartemeldung -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- die Fehlerliste -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- die Schaltfläche -->
<div class="alert alert-warning">
<button class="btn btn-primary" ng-click="getClients()">{{clients.title|translate}}</button>
</div>
<!-- die Kundenliste -->
<list2 model="clients" ng-if="clients.show"></list2>
</div>
...
<script type="text/javascript" src="rdvmedecins-11.js"></script>
<!-- Anweisungen -->
<script type="text/javascript" src="list2.js"></script>
Die Änderungen gegenüber der vorherigen Anwendung sind folgende:
- Zeilen 15–17: Hinzufügen einer Schaltfläche;
- Zeile 20: Verwendung einer neuen Anweisung [list2];
- Zeile 23: Verwendung einer neuen Datei „JS“;
- Zeile 25: Import der Datei „JS“ aus der Direktive „[list2]“;
3.7.13.3. Die Direktive [list2]
Die Anweisung [list2] in [list2.js] lautet wie folgt:
angular.module("rdvmedecins")
.directive("list2", ['utils', '$timeout', function (utils, $timeout) {
// Instanz der zurückgegebenen Richtlinie
return {
// Element HTML
restrict: "E",
// URL des Fragments
templateUrl: "list.html",
// für jede Instanz der Direktive eindeutiger Geltungsbereich
scope: true,
// Funktion zur Verknüpfung mit dem Dokument
link: function (scope, element, attrs) {
utils.debug('directive list2');
scope.model = scope[attrs['model']];
$timeout(function () {
$('#' + scope.model.id).selectpicker('refresh');
})
}
}
}]);
Der einzige Unterschied zur Direktive [list] besteht in Zeile 16: Mit der Methode [selectpicker('refresh')] wird die Komponente [Bootstrap-select] aufgefordert, sich zu aktualisieren. Die Idee dahinter ist, dass jedes Mal, wenn der Benutzer eine neue Kundenliste anfordert, die Dropdown-Liste aktualisiert wird. Das wird zwar nicht funktionieren, aber das ist die Grundidee.
3.7.13.4. Der Controller C
Der Controller befindet sich in der Datei [rdvmedecins-11.js], die durch Kopieren der Datei [rdvmedecins-10.js] erstellt wurde:
// die Kunden
$scope.clients = {title: config.listClients, show: false, id: 'clients', data: []};
...
// Liste der Kunden
$scope.getClients = function getClients() {
// UI wird aktualisiert
$scope.waiting.visible = true;
$scope.errors.show = false;
// die Kundenliste wird angefordert;
task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrClients);
var promise = task.promise;
// das Ergebnis des vorherigen Aufrufs wird analysiert
promise = promise.then(function (result) {
// result={err: 0, data: [client1, client2, ...]}
// result={err: n, messages: [msg1, msg2, ...]}
if (result.err == 0) {
// Die erfassten Daten werden in ein neues Modell eingefügt, um die Ansicht zu aktualisieren
$scope.clients = {title: $scope.clients.title, data: $scope.clients.data.concat(result.data), show: $scope.clients.show, id: $scope.clients.id};
// Das UI wird aktualisiert
$scope.clients.show = true;
$scope.waiting.visible = false;
} else {
// Beim Abrufen der Kundenliste sind Fehler aufgetreten
$scope.errors = { title: config.getClientsErrors, messages: utils.getErrors(result), show: true, model: {}};
// Das UI wird aktualisiert
$scope.waiting.visible = false;
}
});
}
- Zeile 1: Um die Verkettung von Arrays in [clients.data] zu ermöglichen, wird dieses Objekt mit einem leeren Array initialisiert;
- Zeile 18: Die neue Kundenliste wird mit den bereits im Array [clients.data] vorhandenen Listen verkettet;
Zuvor hatten wir geschrieben:
Jetzt schreiben wir:
Um diesen Code zu verstehen, muss man sich daran erinnern, wie das Modell M in der Ansicht V im Fall der Direktive [list2] verwendet wird:
<!-- die Kundenliste -->
<list2 model="clients" ng-if="clients.show"></list2>
Das von der Direktive [list2] verwendete Modell ist [clients]. Es wird in der Ansicht V nur dann neu ausgewertet, wenn sich [clients] im Modell M der Ansicht ändert. Der erste Gedanke, der einem bei der Änderung in den Sinn kommt, ist, Folgendes zu schreiben:
um der Tatsache Rechnung zu tragen, dass die neue Kundenliste zu den bisherigen hinzugefügt werden muss. Dadurch wird [clients.data] geändert, nicht jedoch [clients]. Ich kenne mich mit den Feinheiten von JavaScript nicht aus, aber es wäre nicht verwunderlich, wenn [clients] ein Zeiger wäre, ebenso wie [clients.data]. Der Zeiger [clients] ändert sich nicht, wenn man den Zeiger [clients.data] ändert. Die Anweisung [list2] wird daher nicht neu ausgewertet. Genau das lässt sich beim Debuggen der Anwendung beobachten (F12 in Chrome).
Wenn man schreibt:
$scope.clients = {title: $scope.clients.title, data: $scope.clients.data.concat(result.data), show: $scope.clients.show, id: $scope.clients.id};
stellt man sicher, dass [$scope.clients] tatsächlich einen neuen Wert erhält. Der Zeiger [$scope.clients] verweist auf ein neues Objekt. Die Anweisung [list2] müsste nun neu ausgewertet werden. Dennoch erhält man nicht das gewünschte Ergebnis. Sehen wir uns die Screenshots an, wenn man die Kundenliste zweimal abfragt:
![]() |
- in [1] gibt es nur vier Elemente statt acht;
- in [2] befinden sich diese vier Elemente in einem [select], der jedoch ausgeblendet ist (style='display: none');
![]() |
- In [3] befinden sich die vier Kunden in einer anderen Struktur (HTML), und genau diese sieht der Benutzer, wenn er auf die Dropdown-Liste klickt;
Schließlich geben die Konsolenprotokolle Folgendes an:
- Zeile 1: Der Dienst [dao] wird instanziiert;
- Zeile 2: Der Dienst [dao] erhält eine erste Liste von Clients;
- Zeile 3: Die Anweisung [list2] wird ausgeführt;
- Zeile 4: Der Dienst [dao] ruft eine zweite Kundenliste ab;
Die Anzeige in Zeile 2 stammt aus dem folgenden Code in der Anweisung:
link: function (scope, element, attrs) {
utils.debug('directive list2');
...
}
Betrachten wir den Lebenszyklus der Direktive [list2]:
- Zwischen den Zeilen 1 und 2 ist sie nicht aktiviert, obwohl die Ansicht bereits ein erstes Mal angezeigt wurde. Dies liegt an ihrem Attribut [ng-if="clients.show"] in der Ansicht V:
<list2 model="clients" ng-if="clients.show"></list2>
- Zeile 3: Nach dem Abruf der ersten Ärzte-Liste wird [clients.show] auf „true“ gesetzt und die Anweisung aktiviert;
- nach dem Abruf der zweiten Kundenliste ist zu sehen, dass der Code der Anweisung [list2] nicht aufgerufen wird. Deshalb wird die zweite Liste nicht angezeigt;
Um dieses Problem zu beheben, ändern wir die Direktive [list2] wie folgt:
angular.module("rdvmedecins")
.directive("list2", ['utils', '$timeout', function (utils, $timeout) {
// Instanz der zurückgegebenen Direktive
return {
// Element HTML
restrict: "E",
// URL des Fragments
templateUrl: "list.html",
// für jede Instanz der Direktive eindeutiger Geltungsbereich
scope: true,
// Funktion zur Verknüpfung mit dem Dokument
link: function (scope, element, attrs) {
// Jedes Mal, wenn sich „attrs["model"]“ ändert, muss sich auch die Vorlage der Direktive ändern
scope.$watch(attrs["model"], function (newValue) {
utils.debug("directive list2 newValue", newValue);
// Die Vorlage der Direktive wird aktualisiert
scope.model = newValue;
$timeout(function () {
$('#' + scope.model.id).selectpicker('refresh');
})
});
}
}
}]);
- Zeile 14: Mit der Funktion [scope.$watch] lässt sich ein Wert des Modells beobachten. Ihre Syntax lautet [scope.$watch('var'), f], wobei [var] die Kennung einer Variablen des Modells und f die Funktion ist, die ausgeführt werden soll, wenn sich der Wert dieser Variablen ändert. Hier möchten wir die Variable [clients] überwachen. Daher müssen wir [scope.$watch('clients')] schreiben. Da wir attrs['model']='clients' haben, schreiben wir [scope.$watch(attrs["model"], function (newValue)];
- Zeile 14: Der zweite Parameter der Funktion [scope.$watch] ist die Funktion, die ausgeführt werden soll, wenn sich der Wert der beobachteten Variablen ändert. Der Parameter [newValue] ist der neue Wert der Variablen, also in unserem Fall der neue Wert der Variablen [clients] des Modells;
- Zeile 17: Dieser neue Wert wird dem Feld [model] des Direktivenmodells zugewiesen;
Nach dieser Änderung ändern sich die Protokolle:
![]() |
Oben ist zu sehen, dass nach dem Abruf der zweiten Kundenliste die Direktive [list2] tatsächlich erneut ausgeführt wird, was durch das Ergebnis [2] bestätigt wird.
3.7.14. Beispiel 14: Die Direktiven [waiting] und [errors]
Kehren wir zum Code HTML aus der vorherigen Anwendung zurück:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- die Wartemeldung -->
<div class="alert alert-warning" ng-show="waiting.visible">
...
</div>
<!-- die Fehlerliste -->
<div class="alert alert-danger" ng-show="errors.show">
...
</div>
<!-- die Schaltfläche -->
<div class="alert alert-warning">
<button class="btn btn-primary" ng-click="getClients()">{{clients.title|translate}}</button>
</div>
<!-- die Kundenliste -->
<list2 model="clients" ng-if="clients.show"></list2>
</div>
- Zeilen 5–7: die Wartemeldung;
- Zeilen 10–12: die Fehlermeldung;
Wir beschließen, die Codes HTML dieser beiden Meldungen in Anweisungen zu packen.
3.7.14.1. Der neue Code HTML
Der neue Code HTML [app-24.html] lautet wie folgt:
<div class="container">
<h1>Rdvmedecins - v1</h1>
<!-- die Wartemeldung -->
<waiting model="waiting"></waiting>
<!-- die Fehlerliste -->
<errors model="errors"></errors>
<!-- die Schaltfläche -->
<div class="alert alert-warning">
<button class="btn btn-primary" ng-click="getClients()">{{clients.title|translate}}</button>
</div>
<!-- die Kundenliste -->
<list2 model="clients" ng-if="clients.show"></list2>
</div>
...
<script type="text/javascript" src="rdvmedecins-12.js"></script>
<!-- Anweisungen -->
<script type="text/javascript" src="list2.js"></script>
<script type="text/javascript" src="errors.js"></script>
<script type="text/javascript" src="waiting.js"></script>
- Zeile 5: die Anweisung für die Warte-Meldung;
- Zeile 8: die Anweisung für die Fehlermeldung;
- Zeile 19: die neue, der Anwendung zugeordnete Datei JS;
- Zeilen 21–23: die Dateien JS der drei Anweisungen;
3.7.14.2. Die Anweisung [waiting]
Der Code JS der Anweisung [waiting] befindet sich in der folgenden Datei [waiting.js]:
angular.module("rdvmedecins")
.directive("waiting", ['utils', function (utils) {
// Instanz der zurückgegebenen Richtlinie
return {
// Element HTML
restrict: "E",
// URL des Fragments
templateUrl: "waiting.html",
// für jede Instanz der Direktive eindeutiger Geltungsbereich
scope: true,
// Funktion zur Verknüpfung mit dem Dokument
link: function (scope, element, attrs) {
// Jedes Mal, wenn sich attr["model"] ändert, muss sich auch die Seitenvorlage ändern
scope.$watch(attrs["model"], function (newValue) {
utils.debug("[waiting] watch newValue", newValue);
scope.model = newValue;
});
}
}
}]);
Dieser Code folgt derselben Logik wie der bereits behandelte Code der Anweisung [list2].
In Zeile 8 wird auf die folgende Datei [waiting.html] verwiesen:
<div class="alert alert-warning" ng-show="model.show">
<h1>{{ model.title.text | translate:model.title.values}}
<button class="btn btn-primary pull-right" ng-click="model.cancel()">{{'cancel'|translate}}</button>
<img src="assets/images/waiting.gif" alt=""/>
</h1>
</div>
Im Code JS der Anwendung wird die Vorlage [$scope.waiting] dieses Codes HTML wie folgt definiert:
// die Wartemeldung
$scope.waiting = {title: {text: config.msgWaiting, values: {}}, show: false, cancel: cancel, time: 3000};
3.7.14.3. Die Richtlinie [errors]
Der Code JS der Richtlinie [errors] befindet sich in der folgenden Datei [errors.js]:
angular.module("rdvmedecins")
.directive("errors", ['utils', function (utils) {
// Instanz der zurückgegebenen Direktive
return {
// Element HTML
restrict: "E",
// URL des Fragments
templateUrl: "errors.html",
// für jede Instanz der Direktive eindeutiger Geltungsbereich
scope: true,
// Funktion zur Verknüpfung mit dem Dokument
link: function (scope, element, attrs) {
// Jedes Mal, wenn sich attr["model"] ändert, muss sich auch die Seitenvorlage ändern
scope.$watch(attrs["model"], function (newValue) {
utils.debug("[errors] watch newValue", newValue);
scope.model = newValue;
});
}
}
}]);
Dieser Code folgt derselben Logik wie der bereits untersuchte Code der Direktive [list2].
In Zeile 8 wird auf die folgende Datei [errors.html] verwiesen:
<div class="alert alert-danger" ng-show="model.show">
{{model.title.text|translate:model.title.values}}
<ul>
<li ng-repeat="message in model.messages">{{message|translate}}</li>
</ul>
</div>
Im Code JS der Anwendung wird die Vorlage [$scope.errors] dieses Codes HTML wie folgt definiert:
// Beim Abrufen der Kundenliste sind Fehler aufgetreten
$scope.errors = { title: { text: config.getClientsErrors, values: {}}, messages: utils.getErrors(result), show: true, model: {}};
3.7.15. Beispiel 15: Navigation
Bisher haben wir Anwendungen mit einer einzigen Seite verwendet. In diesem Beispiel befassen wir uns mit mehrseitigen Anwendungen und der Navigation zwischen diesen.
3.7.15.1. Die Ansichten V der Anwendung
![]() |
- in [1], die URL der Ansicht Nr. 1;
- in [2], deren Inhalt;
- in [3] wechselt man zu Seite 2;
- in [4], Ansicht Nr. 2;
- in [5] wechselt man zu Seite 3;
![]() |
- in [6] zur Ansicht Nr. 3;
- in [7] wechselt man zu Seite 1;
- mit [8] kehrt man zur Ansicht Nr. 1 zurück;
3.7.15.2. Struktur des Codes
Wir beginnen mit einer neuen Strukturierung des Codes:
![]() |
- Die Ansichten der Anwendung werden im Ordner [views] abgelegt;
- Das Anwendungsmodul wird im Ordner [modules] abgelegt;
- Die Controller der Anwendung werden im Ordner „[controllers]“ abgelegt;
Ebenso gilt in der endgültigen Version:
- werden die Dienste im Ordner [services] abgelegt;
- Die Direktiven werden im Ordner [directives] abgelegt;
3.7.15.3. Der Container für die Ansichten
Die Ansichten aus dem Ordner [views] werden im folgenden Container [app-25.html] angezeigt:
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
...
</head>
<body>
<div class="container" ng-controller="mainCtrl">
<!-- die Navigationsleiste -->
<ng-include src="'views/navbar.html'"></ng-include>
<!-- die aktuelle Ansicht -->
<ng-view></ng-view>
</div>
...
<!-- das Modul -->
<script type="text/javascript" src="modules/rdvmedecins-13.js"></script>
<!-- die Controller -->
<script type="text/javascript" src="controllers/mainController.js"></script>
<script type="text/javascript" src="controllers/page1Controller.js"></script>
<script type="text/javascript" src="controllers/page2Controller.js"></script>
<script type="text/javascript" src="controllers/page3Controller.js"></script>
</body>
</html>
- Zeile 7: Der Hauptteil des Containers wird von [mainCtrl] gesteuert;
- Zeile 9: Die Anweisung [ng-include] ermöglicht das Einbinden einer externen Datei HTML, in diesem Fall einer Navigationsleiste;
- Zeile 12: Die verschiedenen vom Container angezeigten Ansichten werden innerhalb der Anweisung [ng-view] dargestellt. Im Ergebnis erhalten wir einen Container, der Folgendes anzeigt:
- immer dieselbe Navigationsleiste (Zeile 9);
- verschiedene Ansichten in Zeile 12;
- Zeilen 16–22: Es werden die Dateien JS aus dem Anwendungsmodul [rdvmedecins-13.js] und dessen Controllern importiert;
3.7.15.4. Das Anwendungsmodul
Die Datei [rdvmedecins-13.js] definiert das Anwendungsmodul und die Weiterleitung zwischen den Ansichten:
// --------------------- Angular-Modul
angular.module("rdvmedecins", [ 'ngRoute' ]);
angular.module("rdvmedecins").config(["$routeProvider", function ($routeProvider) {
// ------------------------ Routing
$routeProvider.when("/page1",
{
templateUrl: "views/page1.html",
controller: 'page1Ctrl'
});
$routeProvider.when("/page2",
{
templateUrl: "views/page2.html",
controller: 'page2Ctrl'
});
$routeProvider.when("/page3",
{
templateUrl: "views/page3.html",
controller: 'page3Ctrl'
});
$routeProvider.otherwise(
{
redirectTo: "/page1"
});
}]);
- Zeile 1: Hier wird das Modul [rdvmedecins] definiert. Es hängt vom Modul [ngRoute] ab, das von der Bibliothek [angular-route.min.js] bereitgestellt wird. Dieses Modul ermöglicht die in den Zeilen 6–24 definierte Weiterleitung;
- Zeile 4: Definiert die Funktion [config] des Moduls [rdvmedecins]. Zur Erinnerung: Diese Funktion wird vor jeder Instanziierung eines Dienstes ausgeführt. Es handelt sich um eine Konfigurationsfunktion des Moduls. Hier wird dessen Routing konfiguriert. Dies geschieht mithilfe des Objekts [$routeProvider], das vom Modul [ngRoute] bereitgestellt wird;
- Zeilen 6–10: Definieren die Ansicht, die angezeigt werden soll, wenn der Benutzer den URL [/page1] anfordert. Es handelt sich um eine anwendungsinterne Weiterleitung. Das URL ist eigentlich das [/rdvmedecins-angular-v1/app-21.html#/page1]. Man sieht, dass weiterhin das URL aus dem Container [/rdvmedecins-angular-v1/app-21.html] verwendet wird, jedoch mit einer zusätzlichen Information hinter einem #-Zeichen. Diese zusätzliche Information wird von der Angular-Weiterleitung verwaltet;
- Zeile 8: gibt das Fragment HTML an, das in die Direktive [ng-view] des Containers eingefügt werden soll:
- Zeile 9: gibt den Namen des Controllers dieses Fragments an;
- Zeilen 11–15: Definieren die Ansicht, die angezeigt werden soll, wenn der Benutzer das Fragment URL [/page2] anfordert;
- Zeilen 16–20: Definieren die Ansicht, die angezeigt werden soll, wenn der Benutzer das URL [/page3] anfordert;
- Zeilen 21–24: Definieren die Weiterleitung, die durchgeführt werden soll, wenn die angeforderte URL nicht zu den drei vorherigen gehört (otherwise, Zeile 21);
- Zeile 23: Weiterleitung zu URL [/page1], also zu der in den Zeilen 6–10 definierten Ansicht;
3.7.15.5. Der Controller des Ansichts-Containers
Wir haben gesehen, dass der Ansichtencontainer einen Controller deklariert:
<div class="container" ng-controller="mainCtrl">
Der Controller [mainCtrl] ist in der Datei [mainController.js] definiert:
// Controller
angular.module("rdvmedecins")
.controller('mainCtrl', ['$scope', '$location',
function ($scope, $location) {
// Seitenvorlagen
$scope.page1 = {};
$scope.page2 = {};
$scope.page3 = {};
// globales Template
var main = $scope.main = {};
main.text = "[Modèle global]";
// Für die Ansicht verfügbare Methoden
main.showPage1 = function () {
$location.path("/page1");
};
main.showPage2 = function () {
$location.path("/page2");
};
main.showPage3 = function () {
$location.path("/page3");
}
}]);
- Zeile 3: Der Controller [mainCtrl] benötigt das Objekt [$location], das vom Routing-Modul [ngRoute] bereitgestellt wird. Dieses Objekt ermöglicht den Wechsel zwischen den Ansichten (Zeilen 16, 19, 22);
Kehren wir zum Code des Containers zurück:
<div class="container" ng-controller="mainCtrl">
<!-- die Navigationsleiste -->
<ng-include src="'views/navbar.html'"></ng-include>
<!-- die aktuelle Ansicht -->
<ng-view></ng-view>
</div>
- Der Controller [mainCtrl] erstellt das Modell für den Bereich 1–7;
- die in Zeile 6 eingebundene Ansicht verfügt ebenfalls über einen Controller. Beispielsweise hat die Ansicht [page1] den Controller [page1Ctrl]. Dieser erstellt das Modell des in Zeile 6 angezeigten Bereichs. In diesem Bereich gibt es somit zwei Modelle:
- das vom Controller [mainCtrl] erstellte Modell;
- das vom Controller [page1Ctrl] erstellte Modell;
Es gibt eine Vererbung von Vorlagen. In der in Zeile 6 angezeigten Ansicht sind die Vorlagen der Controller [mainCtrl] und [pagexCtrl] beide sichtbar. Wenn zwei Variablen dieser Vorlagen denselben Namen tragen, überdeckt die eine die andere. Um diese Namenskonflikte zu vermeiden, erstellen wir vier Vorlagen unter vier verschiedenen Namen:
container | mainCtrl | Hand | 11 |
Seite 1 | page1Ctrl | Seite 1 | 7 |
Seite 2 | page2Ctrl | Seite 2 | 8 |
Seite 3 | page3Ctrl | Seite 3 | 9 |
- Zeile 12: definiert ein Element [text] im Modell [main];
Die Zeilen 7–11 haben eine ganz besondere Auswirkung: Sie definieren das Element [$scope] des Controllers [mainCtrl] und erstellen darin vier Variablen [main, page1, page2, page3]. Diese vier Variablen werden als jeweilige Vorlagen für den Container und die drei Ansichten verwendet, die er nacheinander enthalten wird.
3.7.15.6. Die Navigationsleiste
Die Navigationsleiste wird im Container wie folgt definiert:
<div class="container" ng-controller="mainCtrl">
<!-- die Navigationsleiste -->
<ng-include src="'views/navbar.html'"></ng-include>
<!-- die aktuelle Ansicht -->
<ng-view></ng-view>
</div>
Die Navigationsleiste ist in Zeile 3 definiert. Das bedeutet, dass sie nur die Vorlage [main] kennt. Ihr Code lautet wie folgt:
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li class="active">
<a href="">
<span ng-click="main.showPage1()">Page 1</span>
</a>
</li>
<li class="active">
<a href="">
<span ng-click="main.showPage2()">Page 2</span>
</a>
</li>
<li class="active">
<a href="">
<span ng-click="main.showPage3()">Page 3</span>
</a>
</li>
</ul>
</div>
</div>
</div>
- In den Zeilen 16, 21 und 26 werden Methoden des Modells [main] verwendet;
- Zeile 16: Ein Klick auf den Link [Page1] löst die Ausführung der Methode [$scope.main.showPage1] aus. Diese ist im Controller [mainCtrl] wie folgt definiert:
// globales Modell
var main = $scope.main = {};
main.text = "[Modèle global]";
// für die Ansicht verfügbare Methoden
main.showPage1 = function () {
$location.path("/page1");
};
- Zeile 6: Aus dem vorstehenden Code geht hervor, dass die Methode [main.showPage1] in Wirklichkeit die Methode [$scope.main.showPage1] ist. Es ist also tatsächlich diese, die ausgeführt wird;
- Zeile 7: Wir ändern die Methode URL der Anwendung in [/page1]. Kehren wir nun zur Weiterleitung zurück, die im Hauptmodul definiert wurde:
$routeProvider.when("/page1",
{
templateUrl: "views/page1.html",
controller: 'page1Ctrl'
});
Man sieht, dass das Fragment „[views/page1.html]“ in den Container eingefügt wird und dass sein Controller „[page1Ctrl]“ ist.
3.7.15.7. Die Ansicht [/page1] und ihr Controller
Das Fragment [views/page1.html] lautet wie folgt:
<h1>Page 1</h1>
<div class="alert alert-info">
<ul>
<li>Modèle global : {{main.text}}</li>
<li>Modèle local : {{page1.text}}</li>
</ul>
</div>
Wir erinnern uns, dass in der in den Container eingefügten Ansicht das Modell [main] sichtbar ist. Dies soll in Zeile 4 überprüft werden. Außerdem definiert der Controller [page1Ctrl] des Fragments [views/page1.html] ein Modell [page1]. Dieses wird in Zeile 5 verwendet.
Der Code des Controllers [page1Ctrl] lautet wie folgt:
angular.module("rdvmedecins")
.controller('page1Ctrl', ['$scope',
function ($scope) {
// Modell von Seite 1
var page1=$scope.page1;
page1.text="[Modèle local dans page 1]";
}]);
- Zeile 2: Das hier eingefügte [$scope] ist nicht leer. Da der Controller [page1Ctrl] einen Bereich steuert, der in einen von [mainCtrl] gesteuerten Container eingefügt ist, enthält der [$scope] in Zeile 2 die Elemente des [$scope], der durch den Controller [mainCtrl] definiert wurde. Es ist wichtig, dies zu verstehen. Das von dem Controller [mainCtrl] definierte [$scope] enthält die folgenden Elemente von [main, page1, page2, page3]. Das bedeutet, dass man Zugriff auf die Modelle aller Ansichten hat. Das ist nicht unbedingt wünschenswert, trifft hier aber zu. In der endgültigen Version des Angular-Clients werden wir diese Besonderheit nutzen, um im Modell [main] die Informationen zu speichern, die zwischen den Ansichten geteilt werden sollen. Damit haben wir ein Konzept, das dem serverseitigen „Session“-Konzept ähnelt;
- Zeile 6: Wir rufen im [$scope] das Modell [page1] von Seite 1 ab und arbeiten anschließend damit (Zeile 7). Daraufhin erhalten wir folgende Anzeige:
![]() |
Die Ansichten [/page2] und [/page3] basieren auf demselben Modell wie die Ansicht [/page1] (siehe Screenshots auf Seite 240).
3.7.15.8. Steuerung der Navigation
Wir möchten nun die Navigation wie folgt steuern: [page1 --> page2 --> page3 --> page1]. Befindet sich der Benutzer also auf Seite 1 ([/page1]) und gibt er in seinem Browser die Adressen URL oder [/page3] ein, darf diese Navigation nicht akzeptiert werden und der Benutzer muss auf Seite 1 verbleiben.
Um dieses Ergebnis zu erzielen, ändern wir die Seiten-Controller wie folgt:
angular.module("rdvmedecins")
.controller('page1Ctrl', ['$scope', '$location',
function ($scope, $location) {
// Navigation zulässig?
var main = $scope.main;
if (main.lastUrl && main.lastUrl != '/page3') {
// Rückkehr zur letzten URL
$location.path(main.lastUrl);
return;
}
// Die URL der Seite wird gespeichert
main.lastUrl = '/page1';
// Vorlage der Seite
var page1 = $scope.page1;
page1.text = "[Modèle local dans page 1]";
}]);
- Zeile 12: Wenn eine Seite angezeigt wird, speichern wir ihren URL im Modell [main.lastUrl]. Wir nutzen hier das zuvor erwähnte Konzept: die Vorlage [main] zum Speichern von Informationen, die von allen Ansichten gemeinsam genutzt werden. In diesem Fall ist es die zuletzt aufgerufene URL;
- der Code in den Zeilen 4–12 wird dupliziert und an die drei Ansichten angepasst. Hier befinden wir uns in der Ansicht [/page1];
- Zeile 5: Das Modell [main] wird abgerufen;
- Zeile 6: Wenn die Vorlage [main.lastUrl] existiert und sich von [/page3] unterscheidet, ist die Navigation nicht zulässig (die zuletzt aufgerufene URL existiert und ist nicht /page3);
- Zeile 8: Man kehrt dann zur zuletzt besuchten Seite URL zurück;
Probieren wir es einmal aus:
![]() |
- Bei [1] befindet man sich auf Seite 1 und gibt den Code URL von Seite 3 als [2] ein;
- Bei [3] hat die Navigation nicht stattgefunden und man ist zum URL auf Seite 1 zurückgekehrt;
3.7.16. Fazit
Wir haben alle Anwendungsfälle durchgesprochen, die in der endgültigen Version des Angular-Clients vorkommen werden. Bei der Vorstellung des Clients werden wir uns mehr auf die Funktionen der Anwendung als auf deren Implementierungsdetails konzentrieren. Was Letztere betrifft, verweisen wir lediglich auf das Beispiel, das den jeweils behandelten Anwendungsfall veranschaulicht.
3.8. Der endgültige Angular-Client
3.8.1. Projektstruktur
Das endgültige Projekt sieht wie folgt aus:
![]() |
![]() |
- in [1] das gesamte Projekt. [app.html] ist die Master-Seite der Anwendung;
- in [2] befinden sich die Controller;
- in [3] die Direktiven;
- in [4] die Dienste und das Angular-Modul [main.js] der Anwendung;
- in [5] die verschiedenen Ansichten, die in die Master-Seite [app.html] eingebunden werden;
3.8.2. Die Abhängigkeiten des Projekts
Das Projekt weist folgende Abhängigkeiten auf:
![]() |
Die Funktion dieser verschiedenen Elemente wurde in Abschnitt 3.4 auf Seite 134 erläutert.
3.8.3. Die Master-Seite [app.html]
Die Master-Seite lautet wie folgt:
<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
<title>RdvMedecins</title>
<!-- META -->
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Angular client for RdvMedecins">
<meta name="author" content="Serge Tahé">
<!-- der CSS -->
<link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.min.css"/>
<link href="bower_components/bootstrap/dist/css/bootstrap-theme.min.css" rel="stylesheet"/>
<link href="bower_components/bootstrap-select/bootstrap-select.min.css" rel="stylesheet"/>
<link href="assets/css/rdvmedecins.css" rel="stylesheet"/>
<link href="assets/css/footable.core.min.css" rel="stylesheet"/>
</head>
<!-- Controller [appCtrl], Modell [app] -->
<body ng-controller="appCtrl">
<div class="container">
...
</div>
<!-- Bootstrap-Kern JavaScript ================================================== -->
<script type="text/javascript" src="bower_components/jquery/dist/jquery.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap-select/bootstrap-select.min.js"></script>
<script src="bower_components/footable/js/footable.js" type="text/javascript"></script>
<!-- AngularJS -->
<script type="text/javascript" src="bower_components/angular/angular.min.js"></script>
<script type="text/javascript" src="bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.min.js"></script>
<script type="text/javascript" src="bower_components/angular-route/angular-route.min.js"></script>
<script type="text/javascript" src="bower_components/angular-translate/angular-translate.min.js"></script>
<script type="text/javascript" src="bower_components/angular-base64/angular-base64.min.js"></script>
<!-- Module -->
<script type="text/javascript" src="modules/main.js"></script>
<!-- Dienste -->
<script type="text/javascript" src="services/config.js"></script>
<script type="text/javascript" src="services/dao.js"></script>
<script type="text/javascript" src="services/utils.js"></script>
<!-- Direktiven -->
<script type="text/javascript" src="directives/waiting.js"></script>
<script type="text/javascript" src="directives/errors.js"></script>
<script type="text/javascript" src="directives/footable.js"></script>
<script type="text/javascript" src="directives/debug.js"></script>
<script type="text/javascript" src="directives/list.js"></script>
<!-- Controller -->
<script type="text/javascript" src="controllers/appController.js"></script>
<script type="text/javascript" src="controllers/loginController.js"></script>
<script type="text/javascript" src="controllers/homeController.js"></script>
<script type="text/javascript" src="controllers/agendaController.js"></script>
<script type="text/javascript" src="controllers/resaController.js"></script>
</body>
</html>
- Zeile 18: Es ist zu beachten, dass [appCtrl] der Controller der Master-Seite ist;
- Zeilen 19–21: der Inhalt der Master-Seite;
Dieser Inhalt lautet wie folgt:
<div class="container">
<!-- Navigationsleisten -->
<ng-include src="'views/navbar-start.html'" ng-show="app.navbarstart.show"></ng-include>
<ng-include src="'views/navbar-run.html'" ng-show="app.navbarrun.show"></ng-include>
<!-- das Jumbotron -->
<ng-include src="'views/jumbotron.html'"></ng-include>
<!-- der Seitentitel -->
<div class="alert alert-info" ng-show="app.titre.show" translate="{{app.titre.text}}"
translate-values="{{app.titre.model}}"></div>
<!-- Fehler auf der Seite -->
<errors model="app.errors" ng-show="app.errors.show"></errors>
<!-- die Wartemeldung -->
<waiting model="app.waiting" ng-show="app.waiting.show"></waiting>
<!-- die aktuelle Ansicht -->
<ng-view></ng-view>
<!-- Debug -->
<debug model="app" ng-show="app.debug.on"></debug>
</div>
Unabhängig davon, welche Ansicht angezeigt wird, enthält sie immer die folgenden Elemente:
- Zeilen 3–4: eine Steuerleiste. Die beiden Leisten in den Zeilen 3 und 4 schließen sich gegenseitig aus;
![]()
![]()
- Zeile 6: ein Logo bzw. Text der Anwendung:

- Zeile 8: ein Titel

- Zeile 11: eine Fehlermeldung:

- Zeile 13: eine Wartemeldung:

- Zeile 17: eine Debugging-Information:

Alle oben genannten Elemente werden durch die Anweisung [ng-show / ng-hide] gesteuert, die bewirkt, dass sie, selbst wenn sie vorhanden sind, nicht unbedingt sichtbar sind.
3.8.4. Die Ansichten der Anwendung
Im Code der Master-Seite finden wir:
<div class="container">
...
<!-- die aktuelle Ansicht -->
<ng-view></ng-view>
...
</div>
In Zeile 4 werden die verschiedenen Ansichten der Anwendung abgerufen. Diese sind im Modul [main.js] definiert:

Die Rolle der Konfiguration der verschiedenen Routen wurde in Abschnitt 3.7.15.4 auf Seite 242 erläutert.
Die Ansicht [login.html] ist leer, d. h., sie fügt den bereits in der Master-Seite vorhandenen Elementen keine weiteren hinzu.
Die Ansicht [home.html] fügt der Master-Seite das folgende Element hinzu:

Die Ansicht [agenda.html] fügt der Master-Seite das folgende Element hinzu:

Die Ansicht [resa.html] fügt der Master-Seite das folgende Element hinzu:

3.8.5. Funktionen der Anwendung
Die Ansichten des Angular-Clients wurden bereits in Abschnitt 1.3.3 auf Seite 7 vorgestellt. Um das Lesen dieses neuen Kapitels zu erleichtern, werden sie hier noch einmal aufgeführt. Die erste Ansicht lautet wie folgt:
![]() |
- in [6], die Startseite der Anwendung. Es handelt sich um eine Terminvereinbarungsanwendung für Ärzte;
- in [7] ein Kontrollkästchen, mit dem man den Modus [debug] aktivieren oder deaktivieren kann. Letzterer zeichnet sich durch das Vorhandensein des Rahmens [8] aus, der die Vorlage der aktuellen Ansicht anzeigt;
- in [9] eine künstliche Wartezeit in Millisekunden. Der Standardwert beträgt 0 (keine Wartezeit). Wenn N der Wert dieser Wartezeit ist, wird jede Aktion des Benutzers nach einer Wartezeit von N Millisekunden ausgeführt. So lässt sich die von der Anwendung implementierte Wartezeitverwaltung beobachten;
- in [10] die URL des Spring-4-Servers. Folgt man dem Vorhergehenden, ist dies [http://localhost:8080];
- in [11] und [12] die Benutzer-ID und das Passwort desjenigen, der die Anwendung nutzen möchte. Es gibt zwei Benutzer: admin/admin (Login/Passwort) mit einer Rolle (ADMIN) und user/user mit einer Rolle (USER). Nur die Rolle ADMIN ist berechtigt, die Anwendung zu nutzen. Die Rolle USER dient lediglich dazu, zu veranschaulichen, welche Antwort der Server in diesem Anwendungsfall zurückgibt;
- in [13] die Schaltfläche, über die man sich beim Server anmelden kann;
- in [14], die Sprache der Anwendung. Es gibt zwei Sprachen: standardmäßig Französisch und Englisch.
![]() |
- In [1] melden Sie sich an;
![]() |
- Sobald man angemeldet ist, kann man den Arzt auswählen, bei dem man einen Termin vereinbaren möchte ([2]), sowie den Tag des Termins ([3]);
- man fordert in [4] den Terminkalender des ausgewählten Arztes für den gewählten Tag an;
![]() |
- Sobald der Terminkalender des Arztes angezeigt wird, kann man einen Termin reservieren [5];
![]() |
- In [6] wählt man den Patienten für den Termin aus und bestätigt diese Auswahl in [7];
![]() |
Sobald der Termin bestätigt ist, kehrt man automatisch zum Kalender zurück, in dem der neue Termin nun eingetragen ist. Dieser Termin kann später über [7] gelöscht werden.
Die wichtigsten Funktionen wurden beschrieben. Sie sind einfach. Die nicht beschriebenen Funktionen sind Navigationsfunktionen, um zu einer vorherigen Ansicht zurückzukehren. Schließen wir mit der Sprachverwaltung ab:
![]() |
- In [1] wechselt man von Französisch zu Englisch;
![]() |
- mit [2] wird die Ansicht auf Englisch umgestellt, einschließlich des Kalenders;
3.8.6. Das Modul [main.js]
Das Modul [main.js] definiert das Angular-Modul, das die Anwendung steuern wird:
![]() |
- Zeile 4: Das Modul heißt [rdvmedecins];
- Zeile 5: Das Modul [ngRoute] wird für das Routing von URL verwendet;
- Zeile 6: Das Modul [translate] wird für die Internationalisierung der Texte verwendet;
- Zeile 7: Das Modul [base64] wird verwendet, um die Zeichenfolge „login:password“ in Base64 zu kodieren;
- Zeile 8: Das Modul [ngLocale] wird zur Internationalisierung des Kalenders verwendet;
- Zeile 9: Das Modul [ui.bootstrap] wird für den Kalender verwendet;
- Zeile 12: Konfiguration der Routen;
- Zeile 40: die Internationalisierung der Meldungen;
3.8.7. Der Controller der Master-Seite
Zur Erinnerung: der Code HTML der Master-Seite [app.html]:
<body ng-controller="appCtrl">
<div class="container">
...
In Zeile 1 wird der gesamte Body der Master-Seite vom Controller [appCtrl] gesteuert. Aufgrund seiner Position ist er somit der allgemeine und Haupt-Controller der Anwendung. Wie in Abschnitt 3.7.15 erläutert, wird das von diesem Controller erstellte Modell von allen Ansichten geerbt, die in die Master-Seite eingebunden werden.
Sein Code lautet wie folgt:
angular.module("rdvmedecins")
.controller("appCtrl", ['$scope', 'config', 'utils', '$location', '$locale',
function ($scope, config, utils, $location, $locale) {
// Debug
utils.debug("[app] init");
// ----------------------------------------Seiteninitialisierung
// Vorlagen der # Seiten
$scope.app = {waitingTimeBeforeTask: config.waitingTimeBeforeTask};
$scope.login = {};
$scope.home = {};
$scope.agenda = {};
$scope.resa = {};
// Vorlage der aktuellen Seite
var app = $scope.app;
...
// ---------------------------------- Methoden
// aktuelle Aufgabe abbrechen
app.cancel = function () {
...
};
// Abmeldung
app.deconnecter = function () {
...
};
// Dieser Code muss hier stehen bleiben, da er auf die vorangehende Funktion [cancel] verweist
app.waiting = {title: {text: config.msgWaitingInit, values: {}}, cancel: app.cancel, show: true};
}])
;
Die Zeilen 10–14 definieren die fünf Vorlagen, die in der Anwendung verwendet werden:
app.html | appCtrl | |
login.html | loginCtrl | |
home.html | homeCtrl | |
resa.html | resaCtrl | |
agenda.html | agendaCtrl |
Wichtig zu verstehen ist, dass das Objekt [$scope] als Vorlage des Controllers der Master-Seite von allen Ansichten und Controllern geerbt wird. Somit hat der Controller [loginCtrl] Zugriff auf die Elemente von [$scope.app, $scope.login, $scope.home, $scope.resa, $scope.agenda]. Anders ausgedrückt: Ein Controller hat Zugriff auf die Modelle anderer Controller. Die untersuchte Anwendung vermeidet es sorgfältig, diese Möglichkeit zu nutzen. So arbeitet beispielsweise der Controller [loginCtrl] nur mit zwei Modellen:
- seinem eigenen [$scope.login];
- und das des übergeordneten Controllers [$scope.app];
Das Gleiche gilt für alle anderen Steuerungen. Das Modell [$scope.app] wird als gemeinsamer Speicher für die verschiedenen Steuerungen verwendet. Wenn eine Steuerung C1 Informationen an die Steuerung C2 übermitteln muss, wird wie folgt vorgegangen:
In [C1]:
In [C2]:
In beiden Fällen wird $scope vom Controller [appCtrl] geerbt und ist daher in [C1] und [C2] identisch (es handelt sich um einen Zeiger). Das Objekt [$scope.app], das als gemeinsamer Speicher zwischen den Controllern dient, wird in den Kommentaren häufig als session bezeichnet, in Anlehnung an die in klassischen Webanwendungen verwendete Sitzung, die den gemeinsamen Speicher zwischen aufeinanderfolgenden HTTP-Anfragen bezeichnet.
Kehren wir zum Code des Controllers [appCtrl] zurück:
// die Vorlagen der # Seiten
$scope.app = {waitingTimeBeforeTask: config.waitingTimeBeforeTask};
$scope.login = {};
$scope.home = {};
$scope.agenda = {};
$scope.resa = {};
// Vorlage der aktuellen Seite
var app = $scope.app;
// [app.debug] und [utils.verbose] müssen stets synchronisiert sein
app.debug = utils.verbose;
app.debug.on = config.debug;
// derzeit kein Seitentitel
app.titre = {show: false};
// keine Navigationsleisten
app.navbarrun = {show: false};
app.navbarstart = {show: false};
// keine Fehler
app.errors = {show: false};
// Standard-Ländereinstellung
angular.copy(config.locales['fr'], $locale);
// die aktuelle Ansicht
app.view = {url: undefined, model: {}, done: false};
// aktuelle Aufgabe
app.task = app.view.model.task = {action: utils.waitForSomeTime(app.waitingTimeBeforeTask), isFinished: false};
- Zeile 8: [$scope.app] ist die Vorlage für die Master-Seite. Dies ist auch der gemeinsam genutzte Speicher zwischen den verschiedenen Controllern. Anstatt überall [$scope.app.champ=value] zu schreiben, wird der Zeiger [$scope.app] der Variablen [app] zugewiesen, und man schreibt dann [app.champ=value]. Man muss sich lediglich merken, dass „[app]“ das auf der Master-Seite angezeigte Modell ist;
- Zeile 11: [app.debug.on] ist ein boolescher Wert, der den Modus debug der Anwendung steuert. Standardmäßig ist er auf true gesetzt. Sein Wert hängt mit dem Kontrollkästchen [debug] in den Navigationsleisten zusammen;
- Zeile 15: [app.navbarrun.show] steuert die Anzeige der folgenden Navigationsleiste:
![]()
- Zeile 16: [app.navbarstart.show] steuert die Anzeige der folgenden Navigationsleiste:
![]()
- Zeile 18: [app.errors] ist die Vorlage für die Fehlerleiste;

- Zeile 22: [app.view] enthält Informationen zur aktuellen Ansicht, die derzeit durch das Tag [ng-view] der Master-Seite angezeigt wird. Dort werden folgende Informationen vermerkt:
- [url]: das URL der aktuellen Ansicht, zum Beispiel [/agenda];
- [model]: die Vorlage der aktuellen Ansicht, zum Beispiel [$scope.agenda];
- [done]: vrai zeigt an, dass die aktuelle Ansicht ihre Arbeit beendet hat und gerade zu einer anderen Ansicht gewechselt wird;
Diese Informationen dienen der Steuerung der Navigation.
- Zeile 24: Startet eine asynchrone Aufgabe, eine simulierte Wartezeit. Die asynchrone Aufgabe wird durch zwei Zeiger referenziert: [app.view.model.task.action] und [app.task];
Zwei Methoden wurden in den Controller [appCtrl] ausgelagert:
// aktuelle Aufgabe abbrechen
app.cancel = function () {
...
};
// Abmelden
app.deconnecter = function () {
...
};
- Zeile 2: Die Funktion [app.cancel] dient dazu, die aktuelle Aufgabe abzubrechen, für die gerade eine Wartemeldung angezeigt wird. Alle Ansichten zeigen diese Meldung an, daher erfolgt der Abbruch der Aufgabe an dieser Stelle;
- Zeile 7: Die Funktion [app.deconnecter] leitet den Benutzer zurück zur Anmeldeseite. Alle Ansichten außer der Ansicht [/login] bieten diese Möglichkeit;
Die Funktion [app.deconnecter] lautet wie folgt:
// Abmelden
app.deconnecter = function () {
// Zurück zur Anmeldeseite
$location.path(config.urlLogin);
};
- Zeile 4: Man kehrt zur Anmeldeseite von URL [/login] zurück;
3.8.8. Verwaltung der asynchronen Aufgabe
In unserer Anwendung wird zu einem bestimmten Zeitpunkt jeweils nur eine asynchrone Aufgabe ausgeführt. Es ist jedoch möglich, mehrere gleichzeitig auszuführen. Beispielsweise fordert die Anwendung beim Start vom Webdienst zunächst die Liste der Ärzte und anschließend die Liste der Kunden mit zwei aufeinanderfolgenden Anfragen HTTP an. Man könnte dasselbe auch mit zwei gleichzeitigen Abfragen HTTP erreichen. Angular bietet die entsprechenden Werkzeuge für diese Verwaltung. In diesem Fall haben wir uns jedoch gegen diese Vorgehensweise entschieden.
Die aktuell ausgeführte Aufgabe wird mit dem folgenden Code im Controller [appCtrl] abgebrochen:
// Aktuelle Aufgabe abbrechen
app.cancel = function () {
utils.debug("[app] cancel task");
// Die asynchrone Aufgabe der aktuellen Ansicht wird abgebrochen
var task = app.view.model.task;
task.isFinished = true;
task.action.reject();
...
};
- Zeile 5: Die Aufgabe wird in [app.view.model.task] gesucht. Daher stellen alle Controller sicher, dass ihre asynchronen Aufgaben auf dieses Objekt verweisen;
- Zeile 6: um anzugeben, dass die Aufgabe beendet ist;
- Zeile 7: um die Aufgabe mit einem Fehler abzuschließen. Diese Notation unterscheidet sich von der in den untersuchten Angular-Beispielen verwendeten:
- In den Beispielen war das Objekt [task] ein Objekt [$q.defer()], das beendet werden konnte;
- in der endgültigen Version ist das Objekt [task] ein Objekt mit den Feldern [action, isFinished], wobei [action] das Objekt [$q.defer()] ist, dasabgeschlossen werden kann, und [isFinished] ein boolescher Wert ist, der angibt, dass die Aktion abgeschlossen ist;
Betrachten wir den Lebenszyklus des Objekts [task] anhand eines Beispiels. Beim Start übernimmt nach dem Controller [appCtrl] der Controller [loginCtrl] die Kontrolle, um die Ansicht [views/login.html] anzuzeigen. Sein Initialisierungscode lautet wie folgt:
// Das übergeordnete Modell wird abgerufen
var login = $scope.login;
var app = $scope.app;
// aktuelle Ansicht
app.view = {url: config.urlLogin, model: login, done: false};
In Zeile 5 steht [model=login]. Das bedeutet, dass bei einer Änderung des Objekts [login] auch das Objekt [app.view.model] und somit auch [$scope.app.view.model] geändert werden. Wenn man im Controller [loginCtrl] eine simulierte Wartezeit einfügen möchte, schreibt man:
// Simulierte Wartezeit
var task = login.task = {action: utils.waitForSomeTime(app.waitingTimeBeforeTask), isFinished: false};
Durch das Hinzufügen des Feldes [task] zum Objekt [login] wurde es also dem Objekt [$scope.app.view.model] hinzugefügt. Wenn der Benutzer die Warteschlange abbricht, der Code in [appCtrl.cancel]:
// Modell der aktuellen Seite
var app = $scope.app;
...
var task = app.view.model.task;
task.isFinished = true;
task.action.reject();
beendet die simulierte Wartezeit ordnungsgemäß (Zeilen 4–6).
3.8.9. Steuerung der Navigation
Die in der Anwendung verwendeten Navigationsregeln lauten wie folgt:
beliebig | ja | |
/login | ja, wenn der Prüfer [loginCtrl] angegeben hat, dass er seine Arbeit beendet hat | |
/home | ja | |
/agenda | ja | |
/Startseite | ja, wenn der Prüfer [homeCtrl] angegeben hat, dass er seine Arbeit abgeschlossen hat | |
/resa | ja | |
/Agenda | ja | |
/Kalender | ja, wenn der Prüfer [homeCtrl] angegeben hat, dass er seine Arbeit abgeschlossen hat | |
/resa | ja |
Dies wird mit dem folgenden Code implementiert:
Für [agendaCtrl]:

- Zeilen 11–20: Implementierung der Navigationsregel;
- Zeile 26: neue aktuelle Ansicht;
Für [resaCtrl]:

- Zeilen 12–20: Implementierung der Navigationsregel:
- Zeile 27: neue aktuelle Ansicht;
Für [loginCtrl]:

- Hier findet keine Navigationsprüfung statt, da die Regel besagt, dass man von überall her zu URL [/login] gelangen kann. Wenn der Benutzer also diese URL in seinen Browser eingibt, funktioniert dies unabhängig von der aktuell angezeigten Ansicht;
- Zeile 16: die neue aktuelle Ansicht;
Der Code für den Controller [homeCtrl] wurde in Abschnitt 3.8.7 angegeben.
Schließlich für eine Regel wie:
/home | Ja, wenn der Controller [homeCtrl] gemeldet hat, dass er seine Arbeit beendet hat |
Hier ist ein Beispiel für einen Code, der von URL [/home] zu URL [/agenda] wechselt:
![]() |
Oben befinden wir uns in der Methode [afficherAgenda] des Controllers [homeCtrl]. Der Benutzer hat den Terminkalender eines Arztes angefordert.
- Zeile 107: Die Task-Verpflichtung HTTP;
- Zeile 109: Die Variable [app] wurde mit [$scope.app] initialisiert. Dieses Objekt wird, wie wir gesehen haben, als Vorlage für die Ansicht [app.html] verwendet. Diese Vorlage [$scope.app] wird ebenfalls verwendet, um die Informationen zu speichern, die zwischen den Ansichten ausgetauscht werden sollen;
- Zeile 111: Der von der Aufgabe zurückgegebene Fehlercode wird analysiert;
- Zeile 113: Das Ergebnis [result.data] wird in die Vorlage [app] geschrieben;
- Zeile 116: Der Controller [homeCtrl] übergibt die Kontrolle an den Controller [agendaCtrl]. Er teilt ihm mit, dass er seine Arbeit mit dem Code aus Zeile 115 abgeschlossen hat. Dieser Code wird vom Controller [agendaCtrl] wie folgt verarbeitet:

- Zeile 11: Das Objekt [$scope.app.view] wird abgerufen;
- Zeile 15: Auswertung des Feldes [$scope.app.view.done], das durch [homeCtrl] initialisiert wurde;
3.8.10. Die Dienste
![]() |
Die Dienste [config, utils, dao] sind diejenigen, die bereits bei der Vorstellung von Angular beschrieben wurden:
- Der Dienst [config] wurde in Abschnitt 3.7.4 vorgestellt;
- Der Dienst [utils] wurde in Abschnitt 3.7.5 vorgestellt;
- der Dienst [dao] wurde in Abschnitt 3.7.6 vorgestellt;
Zur Erinnerung sei hier noch einmal auf den Aufbau dieser Dienste hingewiesen:
Dienst [config]
![]() |
- in [1]: Man sieht, dass der Code etwa 250 Zeilen umfasst. Der wesentliche Teil dieses Codes besteht in der Auslagerung der Schlüssel für die internationalisierten Nachrichten [2]. Es wird vermieden, diese Schlüssel fest im Code zu hinterlegen;
Service [utils]
![]() |
- Zeile 8: Die Variable [verbose] ist uns bisher noch nicht begegnet. Sie steuert die Funktion [debug] wie folgt:
![]() |
- Zeilen 22–25: Die Funktion [utils.debug] führt keine Aktion aus, wenn [verbose.on] den Wert false annimmt. Diese Variable ist mit einer Variablen des Controllers [appCtrl] verknüpft:
![]() |
- Zeile 21: [app.debug] übernimmt den Wert des Zeigers [utils.verbose]. Daher wird jede Änderung an [app.debug] auch an [utils.verbose] vorgenommen;
- Zeile 22: Der Anfangswert von [app.debug.on] wird aus der Konfigurationsdatei übernommen. Standardmäßig ist dies der Wert true.. Dieser Wert kann sich im Laufe der Zeit ändern. Der Benutzer hat nämlich die Möglichkeit, ihn in den Navigationsleisten zu ändern:
![]() |
- Zeile 45: Über ein Kontrollkästchen (type=checkbox) kann der Wert von [app.debug.on] (Attribut ng-model) geändert werden;
Dienst [dao]
![]() |
3.8.11. Die Anweisungen
![]() |
Die Direktiven [errors, footable, list, waiting] sind diejenigen, die bereits bei der Vorstellung von Angular beschrieben wurden:
- Die Direktive [footable] wurde in Abschnitt 3.7.8.6 eingeführt;
- die Direktive [list] wurde in Abschnitt 3.7.12 eingeführt;
- die Direktiven [errors] und [waiting] wurden in Abschnitt 3.7.14 eingeführt;
Auf die Richtlinie [debug] sind wir bisher nicht gestoßen. Sie lautet wie folgt:
![]() |
Die in Zeile 11 genannte Datei [debug.html] lautet wie folgt:
- Zeile 2: Die Anweisung [debug] zeigt ihre Vorlage im Format JSON in einem Bootstrap-Banner an (Zeile 1);
Diese Anweisung wird nur in der Master-Seite [app.html] verwendet:
![]() |
- Die Anweisung [debug] wird in Zeile 35 verwendet. Sie zeigt daher im Debug-Modus (Attribut ng-show) die Form JSON der Vorlage [$scope.app] an. Das ergibt dann etwa Folgendes:
![]() |
Die Interpretation erfordert fundierte Kenntnisse des Codes, doch sobald diese vorhanden sind, erweisen sich die oben genannten Informationen als nützlich für das Debugging. Hier wurden die Elemente des angezeigten Modells [$scope.app] hervorgehoben. Zur Erinnerung: [$scope.app] ist der von den Controllern gemeinsam genutzte Speicher;
- [waitingBeforeTask]: die simulierte Wartezeit vor jeder Anfrage an HTTP;
- [debug]: der Debug-Modus – muss zwangsläufig true sein, wenn diese Leiste angezeigt wird;
- [navbarrun]: Boolescher Wert, der die Anzeige der folgenden Navigationsleiste steuert:
![]()
- [navbarstart]: Boolescher Wert, der die Anzeige der folgenden Navigationsleiste steuert:
![]()
- [errors]: Vorlage der Direktive [errors];
- [view]: enthält Informationen zur aktuell angezeigten Ansicht;
- [waiting]: Vorlage der Direktive [waiting];
- [serverUrl, username, password]: Anmeldeinformationen für den Webdienst;
- [medecins]: Vorlage für die auf Ärzte angewandte Direktive [list];
- [clients]: dasselbe für Kunden;
- [menu]: steuert die angezeigten Menüoptionen. Diese sind in [navbar-run.html] definiert:

Die Menüoptionen befinden sich in den Zeilen 16, 23, 29 und 36.
- [formattedJour]: Der im Kalender ausgewählte Tag im Format „JJJJ-MM-TT“;
- [agenda]: der Terminkalender des Arztes. Darin gibt es freie (rv==null) und reservierte Termine. Bei letzteren wird der Name des Kunden angezeigt, der die Reservierung vorgenommen hat;
- [selectedCreneau]: der für die Buchung ausgewählte Termin;
3.8.12. Der Controller [loginCtrl]
![]() |
Der Controller [loginCtrl] ist mit der Ansicht [views/login.html] verknüpft, die in Verbindung mit der Master-Seite die folgende Seite erzeugt:

Der Controller [loginCtrl] lautet wie folgt:

- Zeile 13: [login] ist die Vorlage für die aktuelle Ansicht;
- Zeile 14: [app] ist der gemeinsame Speicher der Controller;
- Zeile 16: [app.view] wird mit den Informationen der aktuellen Ansicht gefüllt;
Dieser Initialisierungscode ist in jedem Controller enthalten. Für den Controller C1 einer Ansicht V1 mit dem Modell M1 ergibt sich folgender Initialisierungscode:
- Zeile 18: Man erinnert sich vielleicht daran, dass [appCtrl] eine simulierte Wartezeit gestartet hat, auf die das Objekt [app.task.action] verweist. Man verwendet die [promise] dieser Aufgabe, um auf deren Ende zu warten;
- Zeile 39: Die Methode [login.setLang] verwaltet den Sprachwechsel;
- Zeile 47: Die Methode [login.authenticate] verwaltet die Benutzerauthentifizierung;
Sehen wir uns die wichtigsten Schritte der Authentifizierungsmethode an:

- Zeilen 50–51: [app.waiting] ist die Vorlage für die Ladebalkenanzeige;
- Zeile 53: [app.errors] ist die Vorlage für den Fehlerbanner;
- Zeile 55: Eine simulierte Wartezeit wird ausgelöst. Das Objekt [action, isFinished] wird von [login.task] referenziert und somit, da [app.view.model=login], auch von [app.view.model.task]. Zur Erinnerung: Dies ist die Voraussetzung dafür, dass die Aufgabe abgebrochen werden kann;
- Zeile 57: Nach Ablauf der simulierten Wartezeit werden die Ärzte geladen;
- Zeile 62: Sobald die Ärzte angefordert wurden, wird diese Anforderung analysiert. Wenn die Ärzte bereitgestellt wurden, werden die Kunden angefordert;
- Zeile 83: Die erhaltene Antwort wird analysiert und die endgültige Ansicht angezeigt. Dies geschieht mit dem folgenden Code:

- Zeile 87: Der Boolesche Wert [task.isFinished] wird in den folgenden Fällen auf true gesetzt:
- Der Benutzer hat das Warten abgebrochen;
- die Anfrage der Ärzte ist mit einem Fehler beendet worden;
- Zeilen 91–98: der Fall, in dem Kunden vorhanden waren;
- Zeile 93: [app.clients] ist die Vorlage für die Anweisung [list], die die Kunden in einer Dropdown-Liste anzeigt;
- Zeilen 97–98: Es wird der Wechsel der Ansicht vorbereitet (Zeile 98), doch zuvor wird angegeben, dass der Controller seine Arbeit abgeschlossen hat (Zeile 97). Zur Erinnerung: [$scope.app.view.done] wird für die Navigationssteuerung verwendet;
Wichtig ist hierbei, dass die Ärzte und Kunden im Browser zwischengespeichert wurden. Sie werden fortan nicht mehr vom Webdienst angefordert.
3.8.13. Der Controller [homeCtrl]
![]() |
Der Controller [homeCtrl] ist mit der Ansicht [views/home.html] verknüpft, die in Verbindung mit der Master-Seite die folgende Seite erzeugt:

Die Struktur des Controllers [homeCtrl] ist wie folgt:

- Zeilen 12–20: Dies ist die Navigationssteuerung. Alle Controller verfügen darüber, mit Ausnahme von [loginCtrl], da die Seite [/login.html] ohne Bedingungen zugänglich ist;

- Zeilen 25–28: Hier finden sich Zeilen, die denen im Controller [loginCtrl] ähneln. [home] ist somit die Vorlage für die dem Controller zugeordnete Ansicht;
- Zeile 33: Ein Attribut, auf das wir bisher noch nicht gestoßen sind. Es handelt sich um die Vorlage für die Titelleiste der Ansicht:
![]()
- Zeile 36: [home.datepicker] ist die Vorlage für den Kalender;
- Zeile 38: [app.menu] ist die Vorlage für das Menü der Navigationsleiste. Hier wird die Option [Agenda] vorhanden sein. Mit ihr kann der Terminkalender eines Arztes abgefragt werden;
Schließlich verfügt der Controller über zwei Methoden:

Die Anzeige des Terminkalenders (Zeile 51) wurde in Abschnitt 3.7.8 behandelt.
3.8.14. Der Controller [agendaCtrl]
![]() |
Der Controller [agendaCtrl] ist mit der Ansicht [views/agenda.html] verknüpft, die in Verbindung mit der Master-Seite die folgende Seite erzeugt:

Die Struktur des Controllers [agendaCtrl] ist wie folgt:

- Die Zeilen 10–20 sorgen für die Navigationssteuerung;

- Zeilen 23–26: [agenda] ist die Vorlage für die dem Controller [agendaCtrl] zugeordnete Ansicht;
- Zeilen 36–44: [app.titre] ist die Vorlage für die folgende Titelleiste:

- Zeile 46: Das Menü enthält die Option [Home / Accueil]:
![]()
Die Methoden des Controllers lauten wie folgt:

- Zeile 95: Die Methode [agenda.supprimer] wurde in Abschnitt 3.7.9 behandelt;
Die Methode [agenda.home] ist eine reine Navigationsmethode:

Die Methode [agenda.reserver] lautet wie folgt:

- Zeile 73: Der Parameter der Funktion [reserver] ist die Nummer des Zeitfensters (ID);
- Zeilen 77–86: dienen dazu, das Zeitfenster mit dieser Kennung zu finden;
- Zeile 82: Das gefundene Zeitfenster wird in den gemeinsamen Speicher [app] geschrieben. Der Controller [resaCtrl], der die Kontrolle übernimmt (Zeile 90), nutzt diese Information, um seine Titelzeile anzuzeigen;
- Zeilen 89–90: Navigation zu [/resa.html];
3.8.15. Der Controller [resaCtrl]
![]() |
Der Controller [resaCtrl] ist mit der Ansicht [views/resa.html] verknüpft, die in Verbindung mit der Master-Seite die folgende Seite erzeugt:

Der Controller [resaCtrl] hat folgende Struktur:

- Zeilen 12–20: die Navigationssteuerung;

- Zeilen 24–27: [resa] ist die Vorlage für die aktuelle Ansicht;
- Zeilen 38–45: [app.titre] ist die Vorlage für die folgende Titelleiste:

- Zeile 47: Es werden zwei Menüoptionen angezeigt:
![]()
Die Methoden des Controllers lauten wie folgt:

Die Methode [resa.valider] wurde in Abschnitt 3.7.9 behandelt.
3.8.16. Sprachverwaltung
Alle Controller bieten die folgende Methode [setLang] an:

Sie hätte in den Controller [appCtrl] ausgelagert werden können.




























































































































