Like this paper and download? You can publish your own PDF file online for free in a few minutes!Sign Up
File loading please wait...
Citation preview
Philipp Tarasiewicz
·
Robin Böhm
u ar Eine praktische Einführung in das JavaScript-Framework
dpunkt.verlag
AngularJS
Ü ber die Autoren Philipp Tarasiewicz ist im Web groß geworden und arbeitet als
freiberuflicher Technologieberater, Autor, Sprecher und Coach. Seit einigen Jahren hat er sich auf den Bereich Enterprise JavaScript, ins besondere AngularJS, spezialisiert und unterstützt Unternehmen bei der Aus- und Fortbildung ihrer Mitarbeiter wie auch beim Ramp-up neuer Projekte. Gemeinsam mit Sascha Brink und Robin Böhm betreibt er das deutsche Portal zu AngularJS (AngularJS.DE).
Robin Böhm ist leidenschaftlicher Softwareentwickler, Berater und
Autor im Bereich der Webtechnologien und speziell zu Enterprise JavaScript. Er beschäftigt sich seit einigen Jahren intensiv mit der Erstellung clientseitiger Webapplikationen und unterstützt Unter nehmen sowohl bei der Aus- und Fortbildung von Mitarbeitern als auch bei der Umsetzung von Projekten. Außerdem ist er Mitgründer des Portals AngularJS.DE.
Zu diesem Buch- sowie zu vielen weiteren dpunkt.büchernkönnen Sie auch das entsprechende E-Book im PDF-Format herunterladen. Werden Sie dazu einfach Mitglied bei dpunkt.plus+: www.dpunkt.de/plus
Philipp Tarasiewicz Robin Böhm ·
AngularJS Eine praktische E i nführung i n das JavaScript-Framework
Bibliograf ische Information der Deutschen Nationalbibl iothek Die Deutsche Nationalbibl iothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet ü ber http://dnb.d-nb.de abrufbar.
Die vorliegende Publik ation ist u rheberrechtlich geschützt. Alle Rechte vorbehalten. Die Verwendung der Texte und Abbildungen, auch auszugsweise, ist ohne die schriftliche Zustimmung des Verlags u rheberrechtswidrig und daher strafbar. Dies gilt insbesondere fü r die Vervielfä ltigung, Ü bersetzung oder die Verwendung in elektron ischen Systemen. Es wird darauf hingewiesen, dass die im Buch verwendeten Soft- und Hardware-Bezeichnungen sowie Markennamen und P roduktbezeichnungen der jeweiligen Firmen im Allgemeinen Wa renzeichen-, marken- oder patentrechtlichem Schutz unterliegen. Alle Angaben und P rogramme in diesem Buch wurden mit größter Sorgfalt kontrolliert. Weder Autor noch Verlag können jedoch fü r Schäden haftbar gemacht werden, die in Zusammenhang mit der Verwendung dieses Buches stehen. 5 4 3 21 0
Philipp Tarasiewicz: Für Thaddeus, Sylwia, Patrick und Laura
Robin ßöhm: Für Heike, Ernst, Kathi, Roswitha und Lisa
D ieses K apitel soll i n Form von k urzen Beispielen die mächtigsten Features von AngularJS demonstrieren. Außerdem wollen wir ein ers tes Gefühl dafür vermitteln, wie typische Quellcodefragmente i n ei ner AngularJS-Anwendung a ussehen. Die Beispiele sind a bsichtlich sehr einfach gehalten, um sie ohne tiefe Kenntnisse des Frameworks verste hen und ausführen zu können. Wir werden nicht alle Einzelheiten er k lären, sondern die Erklärungen mit Absicht recht k urz halten. Wenn Sie das Buch durchgearbeitet haben, werden Sie die Beispiele in ihren E inzelheiten verstehen.
1 .1
Zwei-Wege-Date n b i n d u n g : Boilerplate-Code wa r gestern
E ines der ausschlaggebenden Features, die für AngularJS sprechen, ist sicherlich die Zwei- Wege-Datenbindung (Two-Way D ata-Bindi ng). Die Zwei-Wege-Datenbindung sorgt dafür, dass sich Änderungen im Datenmodell automatisch auf die entsprechenden Elemente i n der An sicht a uswirken und sich Benutzerinteraktionen innerhalb der Ansicht a uch automatisch in dem D atenmodell widerspiegeln. D urch diesen a utomatischen Abgleich in beide Richtungen sprechen wir von einer Zwei-Wege-Datenbindung. Somit können wir uns sehr v iel >>Klebe Code
Listing 1 - 1 Zwei-Wege Datenbindung mit einem Eingabefeld
1
2
AngularJS Schnel lstart
Name :
Hel l o { {yourName } } !
Ohne auf die Einzelheiten detailliert einzugehen, sehen wir, dass Lis ting 1-1 eine einfache HTML-Datei enthält, die ein i nput-Eingabefeld beinhaltet und eine Ausgabe in Form einer h l-Überschrift definiert. Wenn wir dieses Beispiel nun in einem Browser ausführen (siehe Ab bildung 1-1 ), dann stellen wir fest, dass sich d ie Überschrift jedes Mal a utomatisch aktualisiert, wenn wir den Text in dem Eingabefeld än dern. Der Mechanism us, der das für uns übernimmt, ist also die Zwei Wege-Datenbindung. Abb. 1 - 1 Ausgabe des Beispiels aus Listing 7-7 in Chrome
C
Cl localhost:9000/2way_binding_simple.htm l � "{:J
{oi)
_
Name: AngularJS
Hello AngularJS! ng-mode/ ist eine Direktive.
Für d ie Herstellung der Zwei-Wege-Datenbindung ist i n diesem Beispiel das Attribut ng-mode 1 verantwortlich. Oder anders erklärt: Das, was für HTML-Kenner aussieht wie ein unbekanntes HTML-Attri but, ist i n der Welt von AngularJS eine sogenannte Direktive. Die ngMo de/-Direktive stellt eine Zwei-Wege-Datenbi ndung zwischen dem Ein gabefeld und der Variable yourName her. Damit ändert s ich der Wert dieser Variable automatisch entsprechend der Eingabe im E ingabefeld. Wo die Variable yourName definiert ist, ist zunächst einmal unerheblich.' Zum Verständnis dieses Beispiels fehlt j etzt noch die Erklärung zu dem Ausdruck in den doppelten geschweiften Klammern innerhalb des 1 Für Ungeduldige: AngularJS nutzt das Vie wM o de l-Konzept zur Realisie rung der Zwei-Wege-Datenbindung. Dabei tragen die sogenannten ViewMo dels innerhalb von AngularJS den Namen Scopes. Die Variable yourName ist also innerhalb eines solchen Scopes definiert.
1 .1
hl-Tags. Dabei handelt es sich um eine sogenannte Expression. Das ist der AngularJS-Mechanismus, mit dem wir Ausgaben produzieren können. I nsbesondere erlauben uns Expressions, Variablenwerte a us zugeben. Sie unterliegen dabei ebenfalls der Zwei-Wege-Datenbindung. Wenn sich also der Wert der Variable yourName verändert, dann wird die Expression neu ausgewertet, was schließlich dazu führt, dass sich die Ansicht automatisch aktualisiert. Auf diese Weise ü berträgt das Frame work den Inhalt des Eingabefeldes automatisch in die Überschrift. Wir sollten an dieser Stelle noch die ng-app-Direktive erwähnen, die in diesem Beispiel das htm 1 -Tag annotiert. Mithilfe dieser Direktive teilen wir AngularJS mit, welchen Teil des D O M (Document Object Model) das Framework als AngularJS-Anwendung betrachten soll. Da durch dass wir die Direktive hier an das html -Tag schreiben, teilen wir AngularJS also mit, dass unsere Anwendung auf dem kompletten DOM operieren soll, weil das html -Tag unser Wurzelknoten ist. Wir könnten die ng-app-Direktive a ber auch an einen tiefer verschachtelten DOM Knoten schreiben und nur für diesen entsprechenden Unterbaum des D O M eine AngularJS-Anwendung a usweisen. Somit würde das Frame work a lle Ausdrücke außerhalb dieses Unterbaums unangetastet lassen und ignorieren. Wir können a uch mehrere Geschwister-Knoten des D O M mit der ng-app-Direktive a nnotieren, um in einem HTML-Dokument mehre re autonome AngularJS-Anwendungen zu definieren. Für den Großteil der Applikationen hat diese Verwendung wenig Sinn, weil die einzel nen Anwendungen z unächst einmal keine Möglichkeiten besitzen, um miteinander zu interagieren. Dennoch sollten wir diesen Aspekt hier erwähnen, weil die offizielle Webseite zu AngularJS2 diese Eigenschaft a usnutzt, um eine Vielzahl von in sich geschlossenen Beispielen z u prä sentieren. Ei n zweites Beispiel
Durch das Feature der Zwei-Wege-Datenbindung lassen sich bereits viele Anwendungsfälle elegant umsetzen, ohne eine einzige Zeile Java Script-Quellcode schreiben zu m üssen. Um das Ganze m it einem wei teren Beispiel zu belegen, sch auen wir uns den nachfolgenden Color Picker an. Ein Color-Picker ist eine UI-Komponente, die dem Benutzer die Auswahl einer Farbe erleichtert, indem sie ihm ein sofortiges visuel les Feedback bezüglich der a ktuellen Belegung der RGBA-Werte liefert.
2
3
Zwei-Wege-Datenbindung: Boilerplate-Code war gestern
http://angularjs.org/
Expressions
ng-app definiert eine AngularJS-Anwendung.
Mehrere AngularJS-An wendungen in einem HTML-Dokument
4
Listing 1-2 Zwei-Wege Datenbindung mit HTML5-Schiebereglern
1
AngularJS Schnellstart
< ! DOCTYPE h tml >
R : G : B : A : < i nput type = " range" name = " col or_a " mi n = " O " max= " l " step= " O . O l " ng -model = " a " >
E ine e infache Umsetzung solch eines Color-Pickers sehen wir in Lis ting 1-2. Vom Grundaufbau entspricht das Beispiel dem Beispiel da vor. Wir definieren vier HTMLS-Schieberegler, i ndem wir innerhalb des i nput -Tags diesmal dem type-Attri but den Wert range zuweisen. D rei Schieberegler brauchen wir für die Steuerung der Rot-, Grün- und Blau-Werte unseres Color-Pickers und einen weiteren für die Steuerung des Alpha-Kanals. Mit den Attributen mi n und max können wir den Minim um- und Maxim umwert des Reglers festlegen. Das s tep-Attribut gibt die Schrittweite an. Auch in d iesem Beispiel n utzen wir die ngMode/-Direktive, um zwi schen den Sch iebereglern und den entsprechenden Scope-Variablen r, g, b und a eine Zwei-Wege-Datenbindung herzustellen. Weiterhin können wir erkennen, dass auch Expressions zum E insatz kommen. D iesmal produzieren wir mit den Expressions aber keine direkte Ausgabe, son dern setzen damit die einzelnen Komponenten der backg round-co 1 o r CSS-Eigenschaft des div-Elements, i n dem die Farbvorschau gerenden werden soll. Das Ergebnis ist recht beeindruckend. Dadurch dass wir zwischen den vier Reglern und den vier Einzelkomponenten der rgba -Eigenschaft
1 .2
!! localhost:9000/2way_binc C
5
D i rektiven: Eigene HTML-Eiemente und Attribute
x
Cl localhost:9000/2way_binding_colorpicker.html
auf Basis der Scope-Variablen r, g, b und a eine Zwei-Wege D atenbindung geschaffen h aben, haben wir mit übersichtlichem Aufwand einen simplen Color-Picker m it Live-Farbvorschau erstellt (siehe Abbildung 1-2 ) . Jedes Mal, wenn wir einen der vier Regler ver schieben, ändert sich entsprechend die Farbe in unserem di v-Element für die Farbvorschau. Im Vergleich z u dem Beispiel davor gibt es hier allerdings noch eine kleine Erweiterung. Wir nutzen die nglnit-Direktive, um die Variablen r, g, b und a mit Initialwerten z u belegen.
Abb. 1-2 Ausgabe des Color-Picker-Beispiels aus Listing 7-2 in Chrome
ng-init zur lnitialisierung von Scope-Variablen in einem Template
H i nweis zur n g l n it-Direktive
Die Definition von Initialwerten innerhalb eines Templates mithilfe von ng-i ni t gehört in größeren Projekten zwar nicht zum guten Ton, weil wir damit einen Teil der Logik ins Template verlagern. Für unsere Zwecke können wir diese Variante aber nutzen, um in dem einfachen Beispiel das Schreiben des äquivalenten JavaScript-Codes zu vermeiden.
1.2
D i rektive n : Eigene HTM L-Eiemente u n d Attri bute
Ein weiteres mächtiges Feature und a ußerdem eines der Alleinstell u ngs merkmale von AngularJS sind Direktiven. Wie wir bereits in den beiden vorangegangenen Beispielen gesehen haben, sind Direktiven a llgegen wärtig in AngularJS. Das Framework selber baut sehr stark a u f diesem Konzept a uf. Mithilfe von Direktiven erweitern wir HTML um eigene Elemente und Attribute. D iesen Mechanism us können wir für viele interessan te Dinge verwerten. Wir können uns z. B. für unsere Anwendung eine
1
6
AngularJS Sch nellstart
eigene Tag-Sammlung definieren, mit der wir große Teile der Anwen dung deklarativ beschreiben können. Oder wir könnten eine eigene wie derverwendbare Bibliothek schreiben, die HTML um eine Vielzahl spe zieller VI-Komponenten erweitert, die in der Domäne unserer Applika tion unverzichtbar sind. Denkbar sind Tags wie , oder a uch < i nput auto-compl ete = " da t a " >. Die entsprechende Logik wür den wir dann innerhalb der Direktiven kapseln. Ein Beispiel
In dem nachfolgenden Beispiel erfinden wir ein -Tag, das die Logik aus dem Color-Picker-Beispiel kapseit und somit wiederver wendbar macht. D a be i haben wir uns dafür entschieden, dass unsere D irektive zwei Anforderungen erfüllen m uss. Es soll eine Möglichkeit geben, die initiale Farbe des Color-Pickers zu konfigurieren. A ußerdem wollen wir bei Farbänderungen von der Direktive benachrichtigt wer den, um darauf in irgendeiner Form reagieren zu können. Das Tem pl ate in Listing 1-3 zeigt, wie wir unsere eigene -Direktive den Anforderungen entsprechend nutzen wollen. Listing 1-3 Verwendung der colorPicker-Direktive
< ! DOCTYPE h tml >
Angul a rJS Schnel l s tart Col orPi c ker
Unsere D i rektive wird genau wie die Standard-HTML-Tags benutzt, i ndem wir die gewöhnliche Tag-Syntax verwenden und einige selbstde finierte Attribute setzen. Im Einzelnen sind das die Attribute i n i t - r, i n i t-g, i n i t - b, i n i t - a und on-change. Entsprechend unserer Anfor derung nutzen wir die i ni t-Attribute, um den initialen R GB- und
1 .2
7
D i rektiven: Eigene HTML-Eiemente und Attribute
Alpha-Wert des Color-Pickers zu konfigurieren. M ithilfe des On-c hange Attributs spezifizieren wir eine Callback-Funktion3 , die die Direktive im Falle eine Farbänderung aufrufen soll. Somit haben wir von a u ßen die Möglichkeit, a uf eine Farbveränderung zu reagieren. Wir tei len der D irektive a lso m it, dass onCo 1 orChange ( ) die Callback-Funktion ist, die die Direktive aufrufen soll . Definiert ist diese Funktion in dem Mai nCtrl -Controller. AngularJS weiß, dass dieser Controller in dem DOM-Unterbaum des -Tags gelten soll, weil wir d ieses Tag mit der ng-contro l l er-Direktive annotiert haben. Was das genau heißt, soll uns an dieser Stelle zunächst nicht weiter interessieren. In dem Template in L isting 1-3 nutzen wir a ußerdem die ngApp D i rektive anders als in den Beispielen davor, nämlich mit der Anga be eines zu ladenden Moduls. Wird - wie in den Beispielen davor kein Modul angegeben, lädt AngularJS für uns ein Default-Modul. Für kleine Beispiele ist das vollkommen ausreichend, aber in größeren An wendungen mit mehreren Modulen funktioniert diese Methode nicht, weil dem Framework nicht klar ist, welches Modul geladen werden soll, nachdem der Browser das DOM vollständig geladen h at. Wir wol len uns ab sofort an Best Practices halten und teilen AngularJS in Lis ting 1-3 m it, dass das Framework das col orPi c kerApp-Modul l aden soll, sobald der Browser das D O M geladen hat. Entsprechend müssen wir dieses Modul und den zuvor genannten Ma i nCtrl -Controller i n unserem JavaScript-Code definieren. var Col orPi c kerApp
=
angul a r . modu l e ( ' col orPi c kerApp ' , [] ) ;
col orPi c kerApp . control l er ( ' Ma i nCtrl ' , funct i on ( $ s cope) $ scope . onCol orChange funct i on ( r , g , b , a ) { consol e . l og ( ' onCo l o rChange ' , r , g , b , a ) ; }; }) ; =
Interessant an dieser Definition ist die Tatsache, dass AngularJS dem Ma i nCt rl -Controller einen Scope ü bergibt. Den Begriff des Scopes ( Geltungsbereich) haben wir i n dem ersten Beispiel bereits eingeführt. Ein Scope definiert alle Variablen und Funktionen, die in einem bestimmten Kontext gültig sein sollen, und ist weiterhin maßgeblich dafür verant wortl ich, dass in d iesem Kontext die Zwei-Wege-Datenbindung genutzt 3Eine Callback-Funktion ist eine Funktion, die eine Komponente A einer Komponente B übergibt, damit A auf bestimmte Ereignisse innerh a l b von B rea gieren kann. Dabei n immt B die Cal l back-Funktion entgegen und ruft sie beim Auftreten bestimmter Ereignisse auf, um A darüber z u informieren. Bildlich ge sprochen ruft die Komponente B die Komponente A im Falle eines bestimmten Ereignisses zurück (deswegen >>Callback«).
Listing 1-4 Definition des colorPickerApp-Moduls inkl. MainCtri Controller
Scope
1
8
Angu larJS Schnellstart
werden kann. In dem Framework gibt es einige Komponenten, für die im Falle eines Aufrufs ein neuer Scope erzeugt wird. Ein Controller ist eine dieser Komponenten. Somit übergibt AngularJS dem Control ler seinen Scope. Auf diesen Scope können wir innerhalb des Control lers mittels des übergebenen $scope-Parameters zugreifen. Hiermit de finieren wir in dem Scope des Mai nCt r 1 -Controllers die angesprochene Callback-Funktion onCo l orChange ( ) , die die vier Parameter r, g, b und a erwartet und von unserer col orPi cker-Direktive bei Farbveränderungen aufgerufen wird. Bei einer Farbveränderung wollen wir a lso lediglich einen Text in der Konsole des Browsers a usgeben. Das D i rektiven-Template
Definition der colorPicker-Direktive
Listing 1-5 Definition des Templates der colorPicker-Direktive
Nun definieren wir die eigentliche co l orPi c ker- Direktive. Dazu fangen wir mit dem Tem plate an, das sich hinter dem -Tag ver bergen soll. R : G : < i nput type = " range" mi n = " O " max = " 2 5 5 " step= " l " ng -model = " g " > B : A :
Wie wir in Listing 1-5 unschwer erkennen können, ist das Template identisch mit dem Template aus dem Color-Picker-Beispiel, bei dem wir keine eigene Direktive definiert haben ( siehe L isting 1-2) . Die Direktivendefinition
Kommen wir nun zu dem i nteressanten Teil des Beispiels, der eigentli chen D i rektivendefinition.
1 .2
Direktiven: Eigene HTML-Eiemente und Attribute
col orPi c kerApp . d i rect i ve ( 1 COl orPi cker 1 , funct i on ( ) { return { s cope : r : 1 @i n i t R 1 , g : 1 @i n i tG 1 , b : 1 @ i n i tß 1 , a : 1 @i n i tA 1 , onChange : 1 & 1 }. restri ct : 1 E 1 , temp l ateUrl : 1 Col orPi c kerTempl ate . html 1 , l i n k : funct i on ( s cope ) { var COLORS = [ 1 r 1 , 1 9 1 , 1 b 1 , 1 a 1 ] ;
9
Listing 1-6 Definition der colorPicker-Direktive
COLORS . forEach ( funct i on (val ue) { s cope . $watch (val u e , funct i on ( newVa l ue , ol dVal ue) i f (newV a l ue !== ol dVal ue) { i f (angul a r . i s Funct i on ( s cope . onChange ) ) { scope . onChange ( generateCo l o rChangeObj ect ( ) ) ;
}); }); var generateCol o rChangeObject var obj = { } ;
funct i on ( ) {
COLORS . forEach ( functi on ( v a l ue) obj [ va 1 ue] s cope [ v a 1 ue] ; }) ; =
ret urn obj ; }; }; }) ;
Wir wollen die D i rektivendefinition in L isting 1-6 nicht im Detail be sprechen, sondern nur auf einige Kernaspekte eingehen, um ein Gefühl dafür zu vermitteln, wie eigene Direktiven implementiert werden kön nen. Wir führen eine neue Direktive mit der d i recti ve { ) -Funktion von AngularJS ein. Der erste Parameter gibt dabei den Namen unserer Direktive an. Dieser Name ist unmittelbar dafür verantwortlich, wie das entsprechende HTML-Tag bzw. HTML-Attribut heißen wird, das die D i rektive in unseren Templates repräsentiert. Dabei lässt sich bereits
Namenskonventionen bei Direktiven
1
10
AngularJS Schnellstart
in d iesem Beispiel eine Besonderheit erkennen. Das HTML-Tag unse res Color-Pickers l autet und der D irektivenname ist co1 orPi c ker. Für die Abbildung von dem D irektivennamen zum HTML Eiement bzw. HTML-Attribut verwendet AngularJS eine interne Regel. Regel zur Benen n u n g von Direktiven
Wenn der Name einer Direktive aus mehreren Wörtern besteht, geben wir ihn mit der Game/Case-Notation an. Die Abbildung zum entsprechen den HTML-Tag bzw. HTML-Attribut erfolgt, indem statt der CameiCase Notation die SnakeCase-Notation verwendet wird. Als Trennzeichen sind dabei der Doppelpunkt ( » : « ) , Bindestrich ( ) und U nterstrich (»_> Boilerplate-Code
Listing 2-6 Ein Template mit einer einfachen Expression
x Wie wir in dem Beispiel in Listing 2-6 erkennen können, ist eine Ex nre sion ein Ausdruck, den wir innerhalb eines Tempiares in doppelten �e chweiften Klammern aufschreiben. Innerhalb dieser Klammern kön '1en wir eine Sub-Menge von JavaScript verwenden, um Ausgaben zu ..., roduzieren. Dabei sollten wir erwähnen, dass der Ausdruck zur Eva _uierung nicht einfach nur durch ein JavaScript eval ( ) eva l uiert wird. omit ergeben sich für Expressions einige Besonderheiten:
l.
Die Auswertung erfolgt n icht wie bei eval () gegen das globale wi ndow-Objekt, sondern gegen den Scope, der im Kontext des Tem piares gültig ist.
, Expressions, die zu undefi ned oder nul l eva l uieren, produzieren kei ne Ausgabe, weil potenzielle Exceptions wie ReferenceError oder Ty peError a bgefangen und ignoriert werden. �-
Kontrollfluss-Anweisungen wie Fallunterscheidungen oder Schleifen können nicht benutzt werden.
tr können in Expressions demnach auch einfache Berechnungen ..:urchführen (siehe Listing 2-7).
o i v>
tr haben bereits erwähnt, dass Expressions in erster Linie den Zweck -:ullen, Scope-Daten in Tempiares auszugeben. In Verbindung mit der Z .,·ei-Wege-Datenbindung heißt das, dass AngularJ S dafür sorgt, dass 'le Expression automatisch neu evaluiert wird, wenn sich der entspre - ende Scope-Wert verändert hat. Somit aktualisiert das Framework .a .. -omatisch unsere Ansichten, wenn sich die zugrunde liegenden Da -'1 verändert haben.
•
Listing 2-7 Eine einfache Berechnung in einer Expression
36
2
Grundlagen und Konzepte des Frameworks
Was Expressions so richtig mächtig macht, ist die Tatsache, dass wir sogenannte Formatierungsfilter nutzen können, um Ausgaben zu formatieren. Eine genauere Erläuterung folgt im anschließenden Unter kapitel. 2.2.6
Fi lter
In AngularJS gibt es zwei Arten von Filtern. Auf der einen Seite haben wir die im letzten Kapitel erwähnten Formatierungsfilter. Auf der an deren Seite befinden sich die Collection-Filter, die in Verbindung mit der ngRepeat-Direktive eingesetzt werden können, um Collections zu filtern bzw. zu transformieren. Collections sind in diesem Zusammen hang entweder Arrays oder Objekt-Hashes. Formatierungsfilter
Wie bereits angesprochen, nutzen wir Formatierungsfilter i nnerhal b von Expressions, um den evaluierten Ausdruck nachträglich z u forma tieren oder zu transformieren. Dazu benutzen wir in Expressions die Pipe-Syntax. Listing 2-8 Eine Expression mit angewandtem Filter
Hel l o , { { user . n ame I u ppercase } } ! F i ve years ago you were { { user . age - 5 } } years ol d .
In dem obigen Beispiel wird auf den eva l uierten Ausdruck user. name der uppercase-Filter angewandt. Dieser Filter ist Tei l der Filtersamm lung, die AngularJS mitbringt, und führt dazu, dass die eingegebene Zeichenkette in Großbuchstaben transformiert wird. Die resultierende Ausgabe in der Ansicht wäre diesem Template zufolge also: Listing 2-9 Resultat des uppercase-Filters
Hel l o , JOHN DOE ! F i ve years ago you were 22 yea rs ol d .
Neben dem uppercase-Filter bringt AngularJS noch weitere nützliche Formatierungsfilter mit, die a l lerdings erst später vorgestellt werden. Darüber hinaus gibt uns das Framework die Möglichkeit, a uch eigene Formatierungsfilter zu definieren.
2.2
Anwendungsbausteine
angul a r . modu l e ( ' myApp ' ) . fi l te r ( ' a l terna t i ngCase ' , funct i on () { ret u rn funct i on ( i nput ) { var output tmp ;
37
Listing 2- 1 0 Unser alternatingCase-Filter
for (var i = 0 ; i < i nput . l engt h ; i ++) { tmp = i nput . charAt ( i ) ; i f ( i % 2 === 0) { output += tmp . toUpperCase ( ) ; e l se { output += tmp . to lowerCase ( ) ;
return outpu t ; }; }) ;
l:ber den Sinn unseres a l ternati ngCase Filters kann man sich streiten. Er oll hier für uns lediglich einen didaktischen Zweck erfü l len. Nun, Jlles, was der F ilter macht, ist bei einer eingegebenen Zeichenkette für eden Buchstaben zwischen Groß- und Kleinschreibung zu alternieren. Die Program mierschnittstelle für Formatierungsfilter ist dabei sehr ein :Jch. Wir erhalten per Funktionsparameter den evaluierten Ausdruck ..1er Expression und können nun innerhalb unserer Filterfu nktion jegli -he Art von Verarbeitung auf Basis der Eingabe vornehmen. Zu guter �.etzt müssen wir innerhalb der Filterfunktion eine Ausgabe zurück _eben, d ie schließlich zur Anzeige gebracht wird. Wir nutzen unseren : ternat i ngCase Filter genauso wie die internen Formatierungsfilter von \ngularJS. -
'
-
< : i v>
Hel l o , { { user . name I a l ternati ngCase } } ! Fi ve yea rs ago you were { { user. age - 5 } } yea rs ol d . < p> � d i v>
Neben den Formatierungsfiltern, die innerhalb von Expressions benutzt werden, gibt es in AngularJ S auch Collection-Filter. Vom API her sind die Collection-Filter zwar genauso aufgebaut wie die Formatierungsfil ter, a ber wir benutzen sie in einem anderen Kontext, nämlich in Kom bination mit der ng-repeat-Direktive. An dieser Stelle müssen wir noch n icht unbedingt wissen, was Di rektiven sind und was wir mit ihnen anstellen können. Wir sollten al lerdings erwähnen, dass die ng-repeat-Direktive dazu genutzt wird, um innerhal b eines Tempiares die Elemente eines Arrays oder eines Objekt Hashes a uszugeben. Wenn man so will, ist ng-repeat a lso das deklara tive Gegenstück zu Schleifen. Listing 2- 13 Verwendung von ng-repeat
Fami 1 y members of { { user . name } } :
{ { member } }
In dem oberen Beispiel referenziert die Scope-Variable fami 1 y ein Array mit Zeichenketten, das die Namen der Familienmitglieder enthält. Die ng-repeat-Direktive sorgt nun dafür, dass in der Ansicht für jedes Ele ment in dem Array ein entsprechendes -Tag gerendert wird. Somit haben wir a ls Resultat eine Ansicht, in der die Familienangehörigen von John Doe angezeigt werden. Der HTML-Code, der sich nach der Auf lösung der ng- repeat-Direktive zur Laufzeit ergibt, sieht a lso in etwa so a us: Listing 2- 14 Resultierender HTML-Code der Ansicht
Fami 1 y members of John Doe :
James Doe C 1 ari ssa Doe Ted Doe
Die Frage, die sich unmittelbar stellt, ist: Können wir diese Ausgabe filtern, sodass nur eine Tei lmenge der Collection zur Ausgabe kommt? Gena u an dieser Stelle kommen die Collection-Filter ins Spiel. Sie sor gen dafür, dass die Collection entsprechend bestimmter Kriterien gefil tert wird oder i hre Elemente auf eine bestimmte Art und Weise trans formiert werden. In Verbindung mit ng- repeat nutzen wir dafü r wieder die Pipe-Notation. Das folgende Beispiel demonstriert den fi 1 ter-Filter
2.2
Anwendung sbausteine
39
on AngularJS. Zugegebenermaßen ist d ieser Filter nicht sehr einfalls ·eich benannt worden, aber er erfüllt seinen Zweck.
Fami 1 y members of { { u ser . name } } :
{ { member } }
< di v>
Listing 2- 15 Verwendung von Filtern innerhalb von ng-repeat
"ir teilen dem fi 1 ter-Filter a lso mit, dass wir nur die Familienmitglie ..:er a usgeben wollen, die das Wort clarissa im Namen enthalten. Dabei nrerscheidet der Filter nicht zwischen Groß- und K leinschreibung. Es gibt noch weitere Collection-Filter im Standardrepertoire von -\ngularJS. Darunter fallen der 1 i m i tTo- und orderßy-Filter, die einer et as einfallsreicheren Namensgebung unterliegen. Wie sich diese beiden .- drer auf die Ausgabe einer Collection a uswirken, sollte anhand des -a mens klar sein. Dennoch werden wir sie später im Buch in unserer neispielapplikation nochmals a u fgreifen und genauer erklären. Wenn d iese Filter für unsere Anwendungsfälle n icht a usreichen, ha . n wir natürlich a uch die Möglichkeit, weitere Collection-Filter zu Jefinieren. Im Folgenden definieren wir einen eigenen Collection-Filter > Look and Fee! und . Wobei bei dem col orPi cker aufgrund der [enge möglicher Trennzeichen noch weitere Aufrufkombinationen 'l t tehen, nämlich: und . Wie eingangs im Schnellstart-Kapitel bereits angerissen, gibt es zwei eitere valide Möglichkeiten, Angular]S-Direktiven a u fzurufen. Und var einerseits mit dem Präfix data- und andererseits mit dem Präfix x - . er Hintergrund ist, dass ältere Browser selbst definierte Tags bzw. At·-·bute nur dann zulassen, wenn sie eines dieser Präfixe besitzen. Dem �ach könnten wir z. B. die spreadsheet-Direktive in unseren Tempiares Jch mittels oder nutzen. Das gilt �arürlich auch gleichermaßen für die fiktive chart-Direktive wie a uch ..:r unseren colorPicker.
•
•
·
> >_
H i nweis zur Benenn u n g von D i rektiven
Es ist wichtig, diese Namenskonvention zu kennen und zu verstehen, um bei der Erstellung eigener Direktiven keine bösen Überraschungen zu er eben. Insbesondere ist es nicht empfohlen, eigene Direktiven so zu be nennen, dass sie mit den impliziten Konventionen kol lidieren. Schnell ver gisst man, dass AngularJS weitere Aufrufmöglichkeiten für uns definiert, und wundert sich dann, warum eine eigene Direktive namens xSpreads heet nicht über das Tag erreichbar ist. Erst der Aufruf mittels führt uns zu dem gewünschten Ergebnis.
51
Listing 2-25 Definition der colorPicker-Direktive
52
2
Grundlagen und Konzepte des Frameworks
Definition mithi lfe einer Link-Funktion
AngularJ S erwartet, dass die Factory-Funktion, d ie wir als zweiten Parameter übergeben, entweder eine Funktion oder ein sogenanntes Direktiven-Definitions-Objekt zurückgibt. Wenn wir eine Funktion zu rückgeben, dann stellt diese Funktion die sogenannte Link-Funktion der Direktive dar. I m Falle des Direktiven-Definitions-Objekts können wir durch eine Menge von Eigenschaften sehr feingranular steuern, wie sich die Direktive verhalten soll. Somit stellt die Rückgabe einer Funktion ( Link-Funktion) den einfachen Fall und die Rückgabe eines Direktiven-Definitions-Objekts entsprechend den komplexen Fall dar. Listing 2-26 Definition der colorPicker-Direktive durch Rückgabe einer Link-Funktion
angul a r . modu l e ( ' myApp ' ) . d i recti ve ( ' col orPi c ker ' , funct i on ( ) { retu rn funct i on ( scope , e l ement , attrs ) { conso l e . l og ( 11 I t ' s me , co l orPi c ker ! 11 ) ; } }) ;
In Listing 2-26 betrachten wir zunächst einmal den einfachen Fall, in dem wir in der Factory-Funktion unserer colorPicker-Direktive eine Funktion, also die sogenannte Link-Funktion, zurückgeben. Die Link Funktion wird für jede Instanz unserer Direktive genau einmal aufge rufen. Somit haben wir innerhalb der Link-Funktion z. B. die Möglich keit, Event-Ha ndler zu registrieren, DOM-Manipulationen zu vollzie hen und sonstige Routinen auszuführen, die instanzbehaftet sind. Kurz um: Die Link-Funktion erlaubt es uns, für jede Instanz einer Direkti ve eine Initialisierungslogik a uszuführen. AngularJS übergibt der Link Funktion beim Aufruf mindestens drei Parameter: 1.
scope - eine Referenz auf den Scope, der für diese Direktive gültig
ist 2. el ement
-
eine Referenz auf das DOM-Element, a u f das die Direktive
wirkt 3 . attrs - eine Referenz auf ein Objekt, mit dessen Hilfe wir auf die HTML-Attribute des DOM-Elements zugreifen können Die Implementierung unserer Link-Funktion in Listing 2-26 macht n ichts, a ußer eine Ausgabe in d ie Konsole des Browsers zu schreiben. Um diese minimale Direktive nun in Aktion zu sehen, müssen wir noch ein kleines Tempiare schreiben, das die Direktive nutzt. Listing 2-27 Nutzung der colorPicker-Direktive in einem Template
2.2
Anwendungsbausteine
53
Li ring 2-27 zeigt solch ein beispielhaftes Template, das unsere mi nimale colorPicker-Direktive nutzt. Wie wir in dem Tempiare erken nen können, haben wir mit unserer minima len Direktivendefin ition ein neu es HTML-Attribute geschaffen. Die Nutzung innerhalb des di v Tags ist dabei willkürlich, und somit könnten wir unser neues Attribut auch in Kombination mit jedem anderen Tag verwenden. Wenn wir al o eine Direktive implementieren, indem wir innerhalb der Factory Funktion lediglich die Link-Funktion zurückgeben, dann erzeugen wir damit standardmäßig ein neues HTML-Attribut. Um das Beispiel nun ausführbar zu machen, müssen wir natürlich noch einen minimalen Ap plikationsrahmen schaffen, indem wir insgesamt drei Dateien erstellen. 1.
i ndex . html : Einstiegspunkt und Basis-Tempiare unserer Anwendung app . j s : Moduldefinition unserer Anwendung
"' col or-pi c ker . j s : Definition unserer colorPicker-Direktive < ! DOCTYPE h tml >
});
});
con t r o l l e r :
' templates/book_de tai l s . html ' , ' BookDetailsCtrl '
An der Route /books/: i sbn, die zu der Detailansicht eines Buches führt, können wir außerdem noch eine kleine Besonderheit erkennen. Mit einem vorangestellten Doppelpunkt können wir sogenannte Pfadpa rameter spezifizieren. Bei dieser Route ist die : i sbn also ein Pfadpa rameter. Das bedeutet, dass dieser Teil der URL variabel sein darf und trotzdem auf die konfigurierte Kombination aus Tempiare und Controller abgebildet wird. Über die : i sbn werden wir also in einem weiteren Entwicklungsschritt auf die Detailinformationen des Buche zugreifen können, das die entsprechende i sbn besitzt. I nnerha lb de BookDeta i l sCt r 1 -Conrrollers erfolgt der Zugriff dann ü ber den sogenannten $routeParams-Service.
3 .4
3 .4.6
Projektstart: Deta i l a nsicht eines Buches
89
Ein eigener Scope m it dem BookDetailsCtri-Controller
In der rudimentären BookDetai l sCtrl -Implementierung, die wir in Lis mg 3-8 sehen, i nteressiert uns die i sbn allerdings noch n icht. Vielmehr • ollen wir i n einem ersten Schritt die Scope-Variable book mit einem Buch-Objekt belegen, sodass wir eine erste lauffähige Version der Deailansicht erzeugen können. Wir erinnern uns an dieser Stelle, dass em Controller für einen DOM-Ausschnitt einen eigenen Scope defi 'llert. Das gilt somit auch für unseren BookDeta i l sCtrl -Cont rolle r . I m DOM-Ausschnitt des Tempiares { book_det a i l s . h tml ) erstellt AngularJS Jl o bei jedem Aufruf der Route einen neuen Scope. Auf diesen Scope önnen wir innerhalb der Konstruktorfunktion des Controllers zugrei ·en, indem wir uns über den Parameter $s cope mithilfe von Dependency • 'l)ection den Scope übergeben lassen. In dem Scope belegen wir sch l ieß -:h die Variable book mit einem Buch-Objekt, das alle Eigenschaften .:mhält, die wir im Tempiare mit der ngBind- bzw. ngBindTemplate rektive ausgeben. : - pp . contro l l er ( ' Boo kDeta i l sCtrl ' , funct i on ( $ s cope) { S scope . boo k { title ' JavaSc r i pt für Enterpri s e - Entwi c k l er ' , subt i t l e ' Profess i onel l p rog rammi e ren i m B rowser und auf dem Server ' , i s bn ' 978-3 -89864- 728- 1 ' ' abst ract ' J avaSc r i pt i st l ängst n i cht mehr n u r für k l a s s i sche Webprog rammi erer i nteres sant . ' , numPages 302 , ' Ol i ver Och s ' , author publ i s her : name ' dpunkt . verl ag ' , u rl : ' http : //dpun kt . de/ ' } L ); =
enn wir in unserem Browser nun eine URL aufrufen, die zu der de .:..,lerten Route passt, dann sollten wir eine erste Version der Detailan ._ht erhalten ( Abbildung 3-5). Eine mögliche URL ist: : : p : //l oca l host : 8080/#/boo ks/123 •
· arürlich können wir anstatt der 123 auch beliebige andere Bei ... elwerte als Pfadparameter übergeben. Dadurch dass wir den i sbn ·adparameter für die Erstellung der Detailansicht noch n icht beachten, .rd immer dieselbe Detailansicht a usgegeben.
$scope
Listing 3-8 Implementierung des BookDetailsCtri-Con trollers
90
3
Abb. 3-5 Ausgabe der Detailansicht eines Buches in Chrome
Das BookMon key-Projekt
() 0
0JsookMonkey c
X
localhost 8080/#/books/123
BookMonkey JavaScript für Euterprise-Entwickler Professionell programmieren im Browser und auf dem Server 978-3-89864-728- 1 302
•
ISB N :
•
Seiten:
•
Autor: Oliver Ochs
•
Verlag: dpunkt.verlag
JavaScript ist längst nicht mehr nur für klassische Webprogrammierer interessant.
3 .4.7
Aufteilung der Tests
Unit-Tests
Der erste Test
Wie bereits erwähnt, liegt es uns sehr am Herzen, den geneigten Leser dieses Buches für den Ansatz der testgetriebenen Entwicklung zu begeis tern . Aus dieser Perspektive betrachtet haben wir daher bereits einen ersten Fehler gemacht, weil wir den Test nicht vor der tatsächlich Im plementierung geschrieben haben. Dennoch wollen wir an d ieser Stelle nachträglich einen ersten einfachen Test sch reiben, um zu zeigen, dass die Ausgaben im Template mithilfe der ngBind- bzw. ngBindTemplate Direktive tatsächlich das gleiche Resultat produzieren wie die A usgaben der Expressions. Für die Tests haben wir bereits in unserem bookmonkeyHauptverzeichnis ein Verzeichnis mit dem Namen test erstellt. In dem test-Verzeichnis erstellen wir nun zwei weitere Unterverzeich nisse mit den Namen un i t und e2e. In dem uni t-Verzeichnis werden wir ab sofort unsere Unit-Tests verwalten, während wir in dem e2e-Verzeichnis die sogenannten E2E-Tests a blegen werden. Zusammengefasst nutzen wir Unit-Tests, um kleine Applikationseinheiten isoliert von dem Rest der Anwendung testen zu können . Sol che Applikationseinheiten sind in der Welt von AngularJS z. B. Con troller, Services, Direktiven oder Filter. Wir versetzen diese Einheiten in eine bestimmte Situation und führen einen Teil ihrer Logik aus. An schließend überprüfen wir, ob das Resultat d ieser Ausführung u nseren Erwartungen entspricht. Um einen echten isolierten Test schreiben zu können, werden die Abhängigkeiten der Applikationseinheiten für ge-
3 .4
Projektstart: Detailansicht eines Buches
wöhnlich gemockt. Das bedeutet, dass wir die echte I mplementierung einer Abhängigkeit durch ein sogenanntes Mock-Objekt a ustauschen. Ein Mock-Objekt ist ein Objekt, das für die zu testende Applikati onseinheit das gleiche öffentliche API bereitstellt, jedoch anstatt einer tatsäeblichen Implementierung eine I mplementierung anbietet, die hart kodierte Werte zurückliefen. Anhand dieser hart kodierten Werte kön nen wir also festlegen, wie sich eine Abhängigkeit im Testfall zu ver halten hat. AngularJS bringt für interne Komponenten bereits eigene :\ lock-lmplementierungen mit, sodass uns an dieser Stelle eine Menge Arbeit beim Testen a bgenommen werden kann. In E2E-Tests hingegen betrachten wir unsere Anwendung sozusaoen als Blackbox. Wir haben keinerlei Zugriff auf den internen Zustand unseres JavaScript-Codes und können somit auch keine Erwartungen auf Basis dieses Zustands formulieren. Vielmehr können wir nur Er wartungen formu l ieren, die sich auf den Zustand des DOM beziehen . .\ lithilfe einer bequemen Domain Specific Language ( DSL) haben wir die Möglichkeit zu steuern, wie sieb ein potentieller Benutzer verhalten oll. Auf diese Weise können wir die Anwendung in eine bestimmte Si
< l i ng-bi nd- templ ate="Autor : { { book . author } } " c l a s s = " bm-boo k -autho r " >
Verl ag :
91
E2E-Tests
Listing 3-9 Erweiterung unseres Templates um aussagekräftige CSS-Kiassen
92
3
Das BookMon key-Projekt
Listing 3-9 zeigt das Markup unseres Templates, nachdem wir die Er weiterungen durchgeführt haben. Wie wir erkennen können, haben wir die HTML-Elemente, die die maßgeblichen Buchinformationen enthal ten, um aussagekräftige CSS-Klassen ergänzt (z. B. bm-book- t i t l e). Das erlaubt uns i m E2E-Test, kurze CSS-Selektoren zu formulieren, um d ie se HTML-Elemente referenzieren und ihren Inhalt überprüfen zu kön nen. Bevor wir uns mit dem eigentlichen E2E-Test befassen, erstellen wir in dem zuvor erzeugten e2e-Verzeichnis ein Unterverzeichnis mit dem Namen temp l ates. Hier legen wir die Datei book_deta i l s . s pec . j s an, in der wir den E2E-Test für die Detailansicht eines Buches definieren werden. H i nweis z u Dateinamen und Struktur für Tests
Es hat sich ••eingebürgert«, alle Dateien, die Tests enthalten, so zu be nennen, dass sie die Endung . s pec . j s besitzen. Außerdem sollte inner halb des test-Ve rzeichnisses für die Tests eine Struktur aufgebaut wer den, die äquivalent zu der eigentlichen Applikationsstruktur in dem app Verzeichnis ist. Listing 3- 1 0 E2E-Test zur Überprüfung der Template-Ausgaben
descri be ( " E2 E : book deta i l s v i ew " , funct i on ( ) { beforeEach ( funct i on ( ) { browser ( ) . navi gateTo ( ' / ' ) ; }) ; i t ( ' shou l d show the correct book det a i l s ' , functi on ( ) { b rowser ( ) . navi g ateTo ( ' #/ books/978-3 -89864-728- 1 ' ) ; expect ( e l ement ( ' . bm-boo k - t i t l e ' ) . html ( ) ) . toBe ( ' J avaSc r i pt für Enterpri s e - Entwi c k l er ' ); expect ( e l ement ( ' . bm-boo k - s u bt i t l e ' ) . html ( ) ) . toBe ( ' Profess i onel l p rog rammi e ren i m Browser und auf dem Server ' ); expect ( e l ement ( ' . bm-book - i s bn ' ) . html ( ) ) . toBe ( ' I SBN : 978-3 -89864-728- 1 ' );
3.4
93
Projektstart: Detailansicht eines Buches
expec t ( e l ement ( ' . bm-book-num- pages ' ) . html ( ) ) . toBe ( ' Sei ten : 302 ' ); expect ( e 1 ement ( ' . bm-boo k -autho r ' ) . h tml ( ) ) . toBe ( ' Auto r : Ol i ver Och s ' };
expect ( e 1 ement ( ' . bm-book -pub 1 i sher- n ame ' ) . html ( ) ) . toBe ( ' dpunkt . verl ag ' }; expect ( e l ement ( ' . bm-book -publ i sher- name ' ) . at t r ( ' h ref ' ) ) . toBe ( ' http : //dpun k t . de/ ' ); expect ( e 1 ement ( ' . bm- book -abst ract ' ) . html ( ) ) . toBe ( ' JavaScri pt i st l ängst n i cht mehr nur fü r ' + ' k l a s s i sche Webprog rammi erer i nteres sant . ' };
l1 ring 3- 1 0 können wir den E2E-Test für die Detailansicht ei
Buches sehen ( book_deta i l s . s pec . j s ) , der prüft, ob die Daten aus .., cope richtig a usgegeben wurden. Die Leser, die mit dem Test - mework ]asmine8 bereits vertraut sind, werden vermutlich sofort Parallelen wiederentdecken. Und in der Tat ist es so, dass die von � _ularJS bereitgestellte DSL für E2E-Tests ( a uch bekannt unter dem n:1en ngScenario) in Anlehnung an Jasmine konzipiert wurde. An er teile sei schon einmal vorweggenommen, dass wir hinterher die '"' ·-Tests mit Jasmine spezifizieren werden. Doch zunächst wollen wir er t einmal wieder unserem E2E-Test widmen, den wir mithilfe von ·enario definiert haben . i nweis auf das Protractor-Framework
.... as Projekt Protractor9 ist ein Framework zur Definition von E2E-Tests. =:s arbeitet ähnlich wie das hier behandelte ngScenario. Allerdings setzt :s auf die WebDriver-Spezifikation auf, die uns deutlich mehr Kontrolle _:Jer die Steuerung des Browsers bietet. ln der offiziellen Dokumentation AngularJS ist erwähnt, dass Protractor Karma als E2E-Umgebung in .-er Zukunft ablösen wird. Unterschiede gibt es in der Konfiguration der -:stumgebung und an einigen Stellen des API. Die hier beschriebenen nzepte zur Definition von E2E-Tests können jedoch analog angewen :s· werden und sind somit generell gültig.
'1trp://pivota l .github.io/j asmine/
ngScenario in Anlehnung an Jasmine
94
3
describe() für Test-Suite
Testfa/1 mit it()
expect()
beforeEach()
Das BookMon key-Projekt
Grundsätzlich sind E2E-Tests syntaktisch genauso aufgebaut wie UnitTests. Außen starten wir mit einem descri be ( ) -Aufruf, der die Test Suite, also eine Sammlung von Testfäl len, definiert. Als ersten Parame ter vergeben wir für die Test-Suite einen Namen. Der zweite Parameter enthält eine anonyme Funktion, innerhalb der wir die Testfälle definieren. Ein einzelner Testfall wird mit der i t ( ) -Funktion spezifiziert, die als ersten Parameter eine Zeichenkette erwartet, die das erwarte te Verhalten in natürlicher Sprache ausdrückt. Als zweiten Parameter übergeben wir wiederum eine anonyme Funktion, in der schließlich der Testfall definiert ist. Charakteristisch für solch einen Testfall ist, dass er zunächst eine Vorbedingung schafft, dann die zu testende Funktio nalität a usführt und schließend mittels einem oder mehreren expect ( ) Aufrufen die erwarteten Nach bedingungen formuliert. Wenn mehrere Testfälle die gleiche Vorbedingung benötigen oder wir eine bestimm te Logik vor jedem Testfall a usführen wollen, dann können wir dazu die beforeEach ( ) -Funktion verwenden, die als einzigen Parameter eine Funktion erwartet, die vor jedem Testfall a usgeführt wird. In unserer E2E-Test-S uite in Listing 3- 1 0 n utzen wir die beforeEach ( ) -Funktion dazu, u m vor jedem Testfall zur Startseite u nserer Anwendung zu navi gieren. Der konkrete Aufruf sieht dazu folgendermaßen a us: browser ( ) . na v i gateTo ( ' / ' )
I n dem momentanen Entwicklungsstadium ist das unsere einzige Vor bedingung, die potenziell für alle Testfälle in dieser Test-Suite relevant sein könnte. Dadurch dass wir aktuell nur einen Testfa l l definiert ha ben, könnten wir diese Vorbedingung a uch unmittelbar in diesen Test fall einkodieren. Allerdings können in der zukünftigen Entwicklung po tenziell noch weitere Testfälle hinzukommen, die diese Vorbedingung benötigen. Vorausschauend, wie wir sind, haben wir deswegen schon einmal diesen beforeEach ( ) -Aufruf eingebaut. In unserem einzigen Testfall navigieren wir mithilfe des folgenden Aufrufs zu der gleichen URL, die wir bereits manuell im Browser ge öffnet haben. Allerdings geben wir d ieses Mal als Pfadparameter eine realistische ISBN an, um diesen Testfall für die zukünftige Entwicklung vorzubereiten. Schließlich werden wir in einem der nächsten Schritte die per URL ü bergebene ISB verwenden, u m die Detailinformationen zu dem entsprechenden Buch anzuzeigen. browser ( ) . na v i g ateTo ( ' #/books/978-3 -89864-728- 1 ' } ;
Innerhalb unserer Applikation sollten jetzt entsprechend unserer Rou tenkonfiguration der BookDeta i 1 sCtrl -Controller und das Template für 9 https://github.com/angu lar/protractor
3.4
Projektstart: Deta i lan sicht ei nes Buches
die Detailansicht ( templ atesjbook_deta i l s . html ) geladen werden. An schließend sollten die Buchinformationen im DOM verfügbar sein. Wir können also anfangen, mittels der expect ( ) -Funktion unsere Nach bedingungen zu formulieren. Aufgrund dessen, dass wir in unserem Template die entsprechenden HTML-Elemente mit aussagekräftigen CSS-Kiassen versehen haben, können wir jetzt ziemlich bequem mit der e l ement ( ) -Funktion in Kombination mit dem entsprechenden CSSelektor (z. B . . bm-boo k - t i t l e ) die nötigen DOM-Elemente referenzie ren. Was nun folgt, ist der Aufruf eines sogenannten Matchers. Ein Y latcher hat innerhalb von ngScenario die gleiche Semantik wie in Jas mine. Er abstrahiert von der eigentlichen Prüfungslogik, indem er die Prüfung durch genau einen Funktionsau fruf repräsentiert. Dabei gibt es ziemlich rudimentäre Mateher wie den toße ( ) -Matcher, der lediglich einen Wertevergleich mit Beachtung des Typs ( ) durchführt. Aber auch komplexere Mateher wie z. B. der toConta i n ( ) -Matcher werden \·on ngScenario bereitgestellt. Dieser Mateher überprüft bei einer Zei chenkette, ob sie Teil einer anderen Zeichenkette ist. Den toContai n ( ) .'datcher können wir a ber a uch verwenden, u m sicherzustellen, dass ein be timmtes Element in einem Array enthalten ist. Eine vollständige Lis te aller Mateher und eine solide Beschreibung des kompletten API von ngScenario finden wir in der offiziellen Dokumentation10 . Wir haben nun eine erste Test-Suite definiert. Um die E2E-Tests der uire auszuführen, benötigen wir jetzt noch eine Ablaufumgebung. Im .-\ngularJS-Umfeld hat sich Karma als Testablau fumgebung etabliert, weil das Projekt praktisch daraus entstanden ist, dass die AngularJS Ent wickler fü r AngularJS selbst auf der Suche nach einem geeigneten \\'erkzeug waren . Karma haben wir zu Beginn des Kapitels bereits inralliert und nutzen es fortan als Ablaufumgebung für unsere Unit- wie .weh E2E-Tests.
95
element()
Mateher
===
H i nweis zu Karma
Dem einen oder anderen ist eventuell die Testablaufumgebung Testa cular bekannt. Testacular wurde in Karma umbenannt, nachdem es die offizielle Testumgebung für das AngularJS-Framework geworden ist.
Cm die in Listing 3- 1 0 definierte Test-Suite auszuführen, benötigen wir ·ur Ka rma jedoch noch eine Konfigurationsdatei. Welche Konfigurati'1 möglichkeiten uns Karma bietet, soll uns an dieser Stelle erst einmal .,Kht weiter interessieren. Dieser Aspekt wird in dem entsprechenden - apitel näher betrachtet (vgl. Abschnitt 5.4). Falls wir unsere Projekt . ·ruktur exakt so erstel lt haben wie bisher beschrieben, können wir pro10 h ttp://docs.angu larj s.orglgu ide/dev_gu ide.e2e-testi ng
Karma als Ablaufumgebung für Tests
96
3
Das BookMon key-Projekt
blemlos die Konfigurationsdatei aus dem offiziellen GitHub-Repository zum Buch verwenden. Dazu kopieren wir die Datei k a rma-e2e . conf . j s i n das bookmonkey- Verzeichnis. Und wenn wir gerade dabei sind, kön nen wir zusätzlich a uch noch die Datei karma . conf . j s in dasselbe Ver zeichnis übertragen . Die Datei karma . conf . j s ist dabei die Konfigurati on für die Ausführung unserer Unit-Tests, die wir im späteren Verlauf noch brauchen werden, während sich in der Datei karma -e2e . conf . j s die Konfiguration für die Ausführu ng der E2E-Tests befindet. Um die E2E-Test-Suite für die Detailansicht eines Buches nun a us zuführen, wechseln wir in das Hauptverzeichnis der BookMonkey Anwendung ( bookmonkey) und starten Karma mit dem folgenden Befehl: karma s ta rt karma -e2e . conf . j s H i nweis zur Ausführung von E2E-Tests
Laut unserer Konfiguration für E2E-Tests ( k a rma -e2e . conf . j s) geht Karma davon aus, dass die BookMonkey-Anwendung über http : I1 1 oca l hos t : 80801 zu erreichen ist. Das bedeutet, dass unser http-se rver-Modul vor der Ausführung von Karma weiterhin auf Port 8080 laufen und unsere Applikation ausliefern muss. Karma lädt die Anwendung über diese URL und führt anschließend die E2 E-Tests aus.
Wenn wir keinen Fehler eingebaut haben, sollte Karma uns ein Feed back über die erfolgreiche Ausführung eines Testfalls geben. Ch rome 30 . 0 . 1 59 9 : Executed 1 of 1 SUCCESS ( 0 . 908 secs I 0 . 699 sec s )
Ziel dieses E2E-Tests war, z u zeigen, dass w i r m i t der ngBind- bzw. ng BindTemplate-D i rektive genauso Ausgaben im Tempiare produzieren können wie mithilfe einfacher Expressions. Um diese Behauptung nun zu verifizieren, können wir unser Tem piare für die Detailansicht eines Buches nochmals auf die Ausgabe mit Expressions zurückstellen. Dabei sollten wir jedoch beachten, dass wir die Auszeichnungen der HTML-Elemente mit den entsprechenden CSS Klassen nicht vergessen. Anderenfalls werden unsere CSS-Selektoren aus dem E2E-Test n icht mehr funktionieren. Das Resultat sehen w1r in Listing 3 - 1 1 . Listing 3- 1 1 Template-Ausgaben produzieren wir testweise mit Expressions.
{ { book . t i t l e } } { { book . subti t l e } }
I SBN : { { book . i s bn } } Sei ten : { { book . n umPages } }
3.4
Projektstart: Detaila nsicht e i nes Buches
Auto r : { { book . author } }
Verl ag :
{ { book . publ i sher . n ame } }
{ { book . abstract } }
Wenn wir den E2E-Test jetzt noch einmal a usführen, werden wir erken nen, dass er trotz der Umstellung auf einfache Expressions weiterhin grün bleibt. Also haben wir an dieser Stelle u nser Ziel erreicht. Zusammenfassung
Wir nutzen sogenannte Expressions, um in einem Tempiare Ausga ben zu produzieren. I nsbesondere können wir mithilfe von Expressions die aktuellen Werte von Variablen aus dem Scope ausgeben, der für das Tem piare im aktuellen Zustand gültig ist. Expressions u nterliegen der Zwei-Wege-Datenbindung. Das bedeu tet, dass sie neu ausgewertet werden, wenn sich der Wert der aus gegebenen Scope-Variable verändert. Wir sollten möglichst versuchen, aus Performance-Gründen die ng Bind- bzw. ngBindTemplate-Direktive den Expressions vorzuzie hen. Mithilfe der ngHref-Direktive können wir Hyperlinks erzeugen, die Expressions enthalten. AngularJS sorgt dann dafür, dass der Link erst aktiv wird, wenn das Template verarbeitet und die entspre chenden Expressions evaluiert wurden. Mit dem $ routeProv i der können wir in einem con fi g ( ) -Block unsere Anwendungsrouten konfigur ieren. Die ngView-Direktive annotiert das HTML-Element, Routen-Tempiares geladen werden sollen.
111
das die
Die sogenannten E2E-Tests erla uben uns, ein simuliertes Benut zerverhalten für unsere Anwendung zu skripten und auf Basis des DOMs gewisse Erwartungen zu formulieren.
97
98
3
Das BookMon key-Projekt
Für E2E-Tests bringt AngularJS ein eigenes Test-Framework mit dem Namen ngScenario mit.
3.5
Listenansicht fü r Bücher
Nachdem wir eine rudimentäre Detailansicht eines Buches erfolgreich erstellt haben, wollen wir uns im nächsten Schritt um die Listenan sicht für Bücher k ümmern. Die Listenansicht soll eine Übersicht von Büchern anzeigen, bevor der Benutzer mit einem Klick auf einen be stimmten Bucheintrag zu der Detailansicht gelangt. Dabei könnten wir die Listenansicht für mehrere Zwecke verwenden. So könnten wir dem Benutzer z. B. die Suchergebnisse einer Büchersuche präsentieren ( vgl. Abbildung 3-6). Aber a uch h i nterher im Administrationsbereich könnte die Listenansicht zum Einsatz kommen, u m dem Admin die Verwaltung zu erleichtern. Abb. 3-6 Skizze der geplanten Listenansicht
Boo kMon key
I
Suche A utor
ISBN
Oliver Ochs
978-3-89864-728- 1
Node.js & Co.
Golo Roden
978-3-89864-728- 1
CoffeeScriQ!
Andreas Schu bert
978-3-89864-728-1
Name
JavaScrigt für Entergrise-Entw.
-- -
--
-- -
----
� --
Der zentraler Aspekt dieses Unterkapitels ist dabei der Umgang mit der ngRepeat-Direktive. Mithilfe dieser D i rektive können wir listenartige Strukturen wie z. B. Arrays ziemlich einfach in einem Template ausge ben. Dabei bietet uns die ngRepeat-Direktive eine ganze Reihe an Funk tionen, um diese Ausgaben filtern oder ordnen zu können. Auch für die Listenansicht formulieren wir wieder eine User Story. Als Nutzer Michael möchte ich mir eine Liste von Büchern ansehen, um mir eine Ü bersicht zu verschaffen.
3.5
3.5.1
Listenansicht für Bücher
99
Als Erstes der Test
Wie bereits erwähnt wollen wir ab sofort mit dem Test-First Ansatz weitermachen. Deswegen widmen wir uns zunächst den Tests für die Listenansicht. Dazu erstellen wir in dem Verzeichnis testle2eltemp 1 atesl die Datei book_1 i s t . s pec . j s, in der wir wieder eine E2E-Test-Suite definieren. descri be ( " E2 E : book 1 i st v i ew " , funct i on () { II Defi ne the a rray of book s i n the expected order . II Sorted by t i t 1 e . var expectedßoo ks [ =
{
t i t 1 e : 1 CoffeeScri pt i sbn : 1 978-3-86490-050- 1 1 , author : 1 Andreas Schubert I ,
1
}, { t i t 1 e : 1 J avaSc ri pt für Enterpri se -Entwi c k 1 er 1 , i sbn : 1 978-3-89864- 728- 1 1 , author : 1 01 i ver Ochs 1 }, {
t i t 1 e : 1 Node . j s & C o . 1 , i sbn : 1 978-3-89864-829-5 1 , autho r : 1 Go 1 o Roden 1
]; II Deri ve an a rray that on 1 y cont a i ns t i t 1 es II for eas i er expectati on checks . var orderedTi t 1 es expectedßooks . map ( functi on ( boo k ) return book . t i t 1 e ; }); =
[ . .] .
); l 1 ring 3- 1 2 zeigt den grundlegenden Aufbau der Test-Suite. Bevor wir
.11e eigentlichen Testfälle schreiben, definieren wir uns zunächst einige Hdfskonstrukte, die h interher die I mplementierung der Testfälle verein ·a.:hen. Das maßgebliche H ilfskonstrukt ist an dieser Stelle das Bücher \rray expectedßooks. In diesem Array definieren wir für jedes Buch, das 1r in der Listenansicht als Eintrag erwarten, ein Buch-Objekt mit den
Listing 3- 12 Vorbereitungen für den E2E-Test zur Überprüfung der Listenansicht
1 00
3
Das BookMon key-Projekt
E igenschaften t i t l e, i sbn und author. Insbesondere ist bei dieser Defini tion wichtig, dass wir die Buch-Objekte in der erwarteten Reihenfolge notieren, damit wir schließlich im Testfall auf bequeme Weise über prüfen können, ob die Listenansicht die Bucheinträge richtig geordnet hat. Hier haben wir uns dafür entschieden, die Reihenfolge lexikalisch anband des Buchtitels festzulegen. Außerdem leiten wir von dem ex p ected B ooks -Array mithilfe der map { ) -Funktion ein zweites Array orderedTi t l es ab, in dem wir le diglich die Buchtitel in der gleichen Reihenfolge a blegen. Auch das o rderedT i t l es-Array ist ein Hilfskonstrukt für die anschließenden Test fälle. Listing 3- 13 Der beforeEach()-8/ock der E2E-Test-Suite zur Überprüfung der Listenansicht
descri be ( " E2 E : book l i st v i ew " , funct i on () { [ . .] .
I n Listing 3- 1 3 sehen wir die Fortsetzung u nserer Test-Suite aus Lis ting 3- 1 2. Genauso wie bei dem E2E-Test für die Detailansicht eines Buches nutzen wir hier einen beforeEach ( ) -Biock, um die Vorbedi ngung für unsere Tests herzustellen. In diesem Fall definieren wir, dass Kar ma vor jedem Testfall zu der Route /books navigieren und anschließend das Dokument aktualisieren soll. Die Aktualisierung ist nötig, weil wir i n einigen zukünftigen Testfällen den Zustand der Ansicht verändern werden. Somit stellen wir mit der Aktualisierung sicher, dass sich die Ansicht vor der Ausführung jedes Testfalls in ihrem Ursprungszustand befindet. Die alleinige Navigation zu der Route /boo ks reicht h ier nicht a us, weil der Ursprungszustand in der Regel nicht hergestellt wird, wenn wir zwischen denselben URLs navigieren. Listing 3- 1 4 Testfall für die Überprüfung der richtigen Anzahl von Büchern
descri be ( " E2 E : book l i s t v i ew " , functi on ( ) { [. . .]
var sel ector
' tabl e . bm-book - l i st t r ' ;
3.5
Listenansicht für Bücher
101
i t ( ' shou l d s how the correct n umber of books ' , funct i on () { expect ( repeate r ( sel ector ) . count ( ) ) . toEq u a l ( expectedßooks . l engt h ) ; }); [. . .] }) ;
Weiter geht
es
Listing 3-14. Da Wir den CSS-Selektor in mehreren Testfällen benötigen, lagern ihn in eine Variable sel ector aus. wir Mit der i t { ) -Funktion schreiben wir nun unseren ersten Testfall, der prüft, ob sich in der Listenansicht tatsächlich so viele Listenein träge befinden, wie wir Buch-Objekte in dem expectedßooks-Array definiert haben . Dazu nutzen wir einen sogenannten Repeater. Der Repeater ist ein Konstrukt, das uns ngScenario bereitstellt, um Informationen aus tabellenartigen DOM-Strukturen, die meistens mithilfe der ngRe peat-Direktive erzeugt wurden, bequemer a uslesen zu können. Solch eine tabellenartige DOM-Struktur ist - wer hätte es gedacht - z. B. eine HTML-Tabelle. Charakteristisch für solch eine Struktur ist, dass sich ein bestimmtes DOM-Muster mehrfach wiederholt. Eine HTMLTabelle ist also ein perfektes Beispiel für solch eine Struktur, weil jede Zeile (
) meistens die gleiche Anzahl von Spalten (
) besitzt und omit eine gleichbleibende Struktur aufweist. Anband des CSS-Selekrors, den wir dem Repeater als Parameter übergeben, haben wir also implizit festgelegt, dass die Listenansicht a u f einer HTML-Tabelle basieren soll. A u f diese Weise können wir mit dem Repeater ziemlich einfach auf Basis des DOM ermitteln, wie viele Bucheinträge unsere L istenansicht beinhaltet. Die entsprechende Repeater-Funktion dafü r lautet count ( ) . Somit können wir nun unsere Erwartung für diesen Test formulieren. Weiter geht es mit dem nächsten Test, der überprüft, ob die Ein träge in der Listenansicht richtig geordnet sind, näml ich lexikalisch ab teigend anband des Buchtitels. m
tabl e . bm-book- l i st t r
Repeater
count() bei Repeatern
1 02
Listing 3- 15 Testfall für die Überprüfung der richtigen Reihenfolge
3
Das BookMon key-Projekt
descri be { " E2 E : book l i s t v i ew " , funct i on ( ) { [. . .]
i t ( ' shou l d show the books i n the p roper order ' , functi on ( ) { II Are t hey i n the expected order II ( ascendi ng sorted by t i t l e ) ? expect ( repeater (sel ector ) . col umn ( ' boo k . t i t l e ' ) ) . toEqual ( o rderedT i t l es ) ; }); [. . .] }); column() bei Repeatern
Der entsprechende Testfall ist i n L isting 3- 1 5 zu sehen. Erneut n ut zen wir einen Repeater, um die entsprechenden Informationen bequem aus dem DOM lesen zu können. Aber diesmal kommt die col umn ( ) Funktion des Repeaters zum Einsatz, an der wir erneut erkennen kön nen, dass ein Repeater praktisch das ergänzende Testkonstrukt zur ngRepeat-Direktive ist. Die col umn { ) - Funktion erwartet als Parame ter einen Datenbindungsausdruck, a lso genau das Fragment, das wir a uch in Verbindung mit der ngBind-Direktive nutzen würden, u m eine Tempiare-Ausgabe zu produzieren. Wir legen an dieser Stelle fest, dass dieser Ausdruck book . t i t l e lauten soll. Somit werden wir hinterher in der Implementierung zum Erzeugen der Ausgabe mit dem folgenden Ausdruck arbeiten müssen: ng-bi nd= " book . t i t l e "
Mithi l fe der co l umn ( ) -Funktion können wir also ziemlich einfach die Werte der Tabellenspalte auslesen, in der der Buchtitel steht. D ie Funk tion liefert uns die entsprechenden Spaltenwerte aller Zeilen als Ar ray. Daher können wir an d ieser Stelle mit dem toEqual ( ) -Matcher ver gleichen, ob das zurückgelieferte Array unserem orderedTi t l es-Array entspricht. Wenn d iese Erwartung erfüllt ist, dann bedeutet das, dass die Listeneinträge der Sortierung entsprechen, die wir manuell in dem expectedßooks-Array festgelegt haben, weil das o rderedTi t l es-Array aus dem expectedßoo ks-Array a bgeleitet wurde.
3.5
Listenansicht für Bücher
1 03
H i nweis zum toEquai ()-Matcher
Der toEqua l () -Matcher führt einen Vergleich auf Basis der AngularJS Funktion angu l a r . equa l s ( ) durch. Diese Funktion nutzt einen etwas komplexeren Algorithmus, um die Gleichheit zwei er Objekte zu überprü fen. Unter anderem prüft sie bei Arrays, ob die einzelnen Elemente über einstimmen. Diese Tatsache erlaubt u ns in Kombination mit den Repea tern eine relativ komplexe Erwartung ziemlich einfach aufzuschreiben.
Was jetzt noch fehlt, bevor wir zur eigentlichen Implementierung der Listenansicht voranschreiten, ist der letzte Testfall, der überprüft, ob neben dem Buchtitel auch die anderen Buchinformationen (Autor und ISBN) richtig a usgegeben wurden. Diesen Testfall sehen wir in Listing 3 - 1 6 . descri be ( " E2 E : book l i st v i ew " , funct i on ( ) l [. . .]
Listing 3-16 Testfall für die Überprüfung der richtigen Listeninhalte
i t ( ' shou l d s how the correct book i nfo rma t i on ' , funct i on ( ) I I Do t h e other book deta i l s ( i sbn , author) match? for ( v a r i 0 , n = expectedBooks . l engt h ; i < n; i ++) expect ( repeater (sel ector) . row ( i ) ) . toEqua l ( =
[
expectedßooks [ i ] . t i t l e , expectedBooks [ i ] . author , expectedßooks [ i ] . i sbn ); } }) ; }) ;
Die Besonderheit an diesem Testfall ist, dass wir einen neuen Aspekt des Repeaters nutzen, nämlich die row ( ) -Funktion. Der Name sagt eigentlich schon alles. D iese Funktion erwartet als Parameter den Index einer Tabellenzeile und gibt uns als Rückgabe die Spaltenwerte dieser Zeile i n Form eines Arrays. Daher können wir mithilfe einer for-Schleife relativ einfach die E rwartung ausdrücken, dass der Buchtitel, der Aurar und die ISBN in einer Zeile i mmer äquivalent zur entsprechenden Definition im expectedßoo ks-Array sein müssen. Damit hätten wir also .weh die letzte Erwartung an die Listenansicht definiert. Darum können wir jetzt versuchen, den Erwartungen entsprechend eine Implementierung zu erstellen. Vorher sollten wir die E2E-Tests allerdings nochmals
row() bei Repeatern
1 04
3
Das BookMon key-Projekt
ausführen und sicherstellen, dass die Tests aus der soeben erstellten Test-Suite fehlschlagen, weil noch keine I mplementierung existiert. karma start karma-e2e . con f . j s
Das entsprechende negative Feedback sollte i n etwa so aussehen: C h rome 30 . 0 . 1 599 : Executed 4 of 4 (3 FA I LED) ( 1 . 2 1 7 secs I 0 . 9 7 1 secs)
Unser E2E-Test für die Detailansicht ist weiterhin »grün « , während die drei Tests, die wir soeben definiert haben, fehlschlagen. In der Ausgabe können wir außerdem nachlesen, welche Tests im Detail fehlgeschlagen sind. 3.5.2
Die Infrastruktur für die Listenansicht
Nachdem wir die E2E-Tests für die Listenansicht definiert haben, wol len wir zunächst noch die Infrastruktur schaffen, bevor wir mit der eigentlichen I mplementierung loslegen. Das bedeutet, dass wir die ent sprechenden Dateien anlegen und in unserer app . j s eine zusätzliche Route konfigurieren müssen. Wir fangen mit der Route an. Dazu müs sen wir die app . j s folgendermaßen erweitern ( Listing 3- 1 7) : Listing 3- 1 7 Eine neue Route für die Listenansicht
v a r bmApp = angu l a r . modu l e ( 1 bmApp 1 , [ 1 ngRoute 1 ] ) ; bmApp . confi g ( funct i on ( $ routeProv i de r ) { $ routeProv i der . when ( 1 /boo ks / : i sbn 1 , { temp l ateUrl : 1 temp l ates /book_deta i l s . html 1 , cont rol l er : I Boo kDeta i l sCt r l 1 }) . when ( 1 /books temp l ateUrl : 1 temp l ates /book_l i s t . h tml 1 , control l er : 1 Book li stCtrl 1 }); }); 1
,
Mithilfe der when-Funktion konfigurieren wir eine weitere Route, so dass alle Aufrufe von /books zu der Listenansicht führen, die natür lich wieder ein Tempiare (book_l i st . html ) und einen eigenen Controller ( Bookl i stCtrl - Co nt roller) besitzt. Somit müssen wir entsprechend die se beiden Dateien erzeugen und in unsere i ndex . h tml mittels zusätz l icher -Tags einbinden. Die Datei book_l i s t . html (Template) erstellen wir in dem Verzeichnis appjtempl ates/, während wir für den BookL i stCtrl -Controller ebenfalls eine separate Datei mit dem Namen book_l i s t . j s in dem Verzeichnis appjsc ri pt s/control l ers/ erstellen.
3.5
3.5.3
Listenansicht für Bücher
1 05
Der BooklistCtri-Controller
Die Implementierung des Boo k l i stCtr 1 -Controllers ist genauso wie beim BookDeta i 1 s C t r1 Controller bisher noch recht simpel gehalten ( Listing 3- 1 8 ) . -
bmApp . contro1 1 e r ( 1 Bookli stCtr1 1 , funct i on ( $ scope) { $ s cope . boo ks [ { t i t 1 e : 1 J avaSc ri pt für Enterpri se- Entwi c k 1 er 1 , i sbn : 1 978-3-89864- 728- 1 1 , author : 1 01 i ver Ochs 1 , [ .] }, =
.
{
.
t i t 1 e : 1 Node . j s & Co . i sbn : 1 978-3-89864-829-5 1 , author : Go 1 o Roden 1 , [. . .] 1 ,
1
}
{
'
t i t 1 e : 1 CoffeeScri pt 1 , i sbn : 1 978-3-86490-050- 1 1 , author : 1 And reas Schubert 1 , [ . ] .
});
.
];
Wir belegen die Scope-Variable boo ks mit einem Array von Buch Objekten. Bevor wir diese Informationen in einem zukünftigen Schritt vom Server laden werden, geben wir uns an dieser Stelle vorerst mit einer statischen Definition zufrieden. Wir sollten außerdem beachten, dass in Listing 3- 1 8 zugunsren der Übersichtlichkeit a bsichtlich die Buchinformationen ausgelassen wurden, die für die Listenansicht nicht relevant sind. Nachdem wir in Listing 3- 1 8 eine Liste von Büchern definiert ha ben, wollen wir diese nun in unserem Template a usgeben. 3.5.4
Die ng Repeat-Direktive: Ausgabe eines Arrays im Template
Um listenartige Struktu ren, also z. B. At-rays, in einem Tempiare auszu geben, verwenden wir die ngRepeat-Direktive. Neben der reinen Ausga be bietet uns AngularJS mit dieser D i rektive a ußerdem d ie Möglichkeit,
Listing 3-18 Der BookListCtri-Contro/ler
1 06
3
Das BookMon key-Projekt
die zugrunde liegende Liste zu filtern, zu transformieren oder zu ord nen. All diese Aspekte wollen wir uns nun der Reihe nach anschauen. Dazu implementieren wir zunächst die einfachste Version u nseres Templates, sodass zumindest schon die drei Bücher aus dem Array a us gegeben werden. Diese Version des Tempiares ist i n Listing 3- 1 9 zu sehen. Listing 3- 1 9 Das Template für die Listenansicht mit Benutzung der ngRepeat-Direktive
ngRepeat als Attribut
Genauso wie im E2E-Test spezifiziert, bauen wir die Listenansicht in Form einer HTML-Tabelle ( ) a uf. D iese Tabelle annotieren wir a ußerdem mit der CSS-Kiasse bm- book - 1 i st . Auch das haben wir im E2E-Test so spezifiziert. Nun kommt der interessante Teil ins Spiel, nämlich die N utzung der ngRepeat-Direktive. Wie wir in Listing 3- 1 9 nachvollziehen können, verwenden wir die ngRepeat-Di rektive als At tribut in Verbindung mit dem HTML-Element, das bei der Ausgabe des books -Arrays mehrfach erzeugt werden soll. Den DOM-Unterbaum des HTML-Elements einschließlich des HTML-Eiements selbst betrachtet die Direktive dabei als Template, welches schl ießlich für jedes Element aus dem zugrunde liegenden Array einmal instanziert wird. In diesem Fall wollen wir a lso erreichen, dass für jedes Buch-Objekt aus dem books-Array eine Tabellenzeile (
) gerendert wird, weil wir die Di rektive in Verbindung mit dem -Tag verwenden. Die ngRepeat- D i rekti ve erwartet dabei als Wert einen strukturier ten Ausdruck wie in d iesem Fall z. B. book i n books. Sie beschreibt, wel ches Array a usgegeben werden soll und wie die Schleifenvariable heißen soll, mittels der wir in jeder Iteration Zugriff auf das aktuell a usgege bene Buch-Objekt erhalten. Wir spezifizieren demnach, dass das a us zugebende Array von der Scope-Variable book s referenziert wird, und benennen die Schleifenvariable book. Somit haben wir bei jeder Iteration über die Schleifenvariable book einen Zugriff auf die einzelnen Buchin formationen wie Buchtitel ( boo k . t i t l e ) , Autor { boo k . author ) und ISB { boo k . i sbn ) . Dieser Zusammenhang wird i n Abbildung 3-7 noch ein mal verdeutlicht. Außerdem können wir in dieser Abbildung erkennen, in welchem DOM-Ausschnitt der Scope des Bookl i stCtrl -Controllers gültig ist.
3.5
� � - - back < t able
�
� . r �- - - >
II
class= " bm-bo ok - l i s t " >
b t} t) k 1 i� t .
:-;;
- ;;;:,� ---------:
bmApp . c o n t r o l l e r ( ' BookListCtrl ' ,
< tbody>
< t r ng- .::e peat= " book i n books " >
--;;;::;:,;;;:_;,:;;
1 07
Listenansicht für Bücher
:
function
-
Entenrl P E
( $ scope)
-
Abb. 3-7 ngRepeat-Oirektive gibt books-Array aus dem Controller-Scope aus.
,,
$ s cope . books
(
)
{
'"' •
_
. f e
=
[ t
c: r
t
pt · , )
I.
tw
"' er • 1 ) ,
;
)I;
< / tbody>
< / table>
Folglich können wir wieder mit der ngßind-Direktive a rbeiten, um die tatsächliche Ausgabe zu produzieren. Die Beziehung zwischen der ngRepeat-Direktive und dem Repeater aus dem E2E-Test können w1r in Abbildung 3-8 noch einmal genau nachvollziehen. L
In Abbildung 3-9 können wir außerdem erkennen, dass AngularJS in j edem der durch die ngRepeat-Direktive erzeugten Scopes die Scope Variable book definiert. Sie referenziert das entsprechende Buch-Objekt.
3.5
Listenansicht für Bücher
1 09
H i nweis zur Ausgabe von Objekten mittels n g-repeat
ln JavaScript verwenden wir oftmals Objekte, um Hashtable-artige Struk turen abzubilden. Auch solche Objekte können mithilfe der ngRepeat Direktive ausgegeben werden . Allerdings lassen sich in diesem Fall die Filterfunktionantäten der Direktive, die wir in einem der nächsten Schrit te nutzen werden, nicht problemlos verwenden. Zu dieser Problema tik haben wir auf AngularJS.DE einen Artikel verfasst, der den H inter grund dieser Einschränkung näher beleuchtet. Die URL zum Artikel lau tet: h t t p : I I angu 1 a rj s . del a rt i ke 1 I angu 1 a rj s - ng - repeat .
Um die Listenansicht nun aufzurufen, rufen wir im Browser die folgen de URL auf: http : ll1 oca1 host : 8080I # I books
Wenn wir keinen Feh ler gemacht haben, sollten wir die Listenansicht sehen (vgl. Abbildung 3- 1 0): 0
0
� BookMonkey c
Abb. 3- 1 0 Ausgabe unserer Listenansicht für Bücher in Chrome
X
localhost. 8080 /# / books
BookMonkey JavaScript für Enterprise-Entw ickler Oliver Ochs
978-3-89864-728- 1
Golo Roden
978-3-89864-829-5
Node.js & Co. CoffeeScript
Andreas Schubert 978-3-86490-050-1
Wenn wir die gleiche Ausgabe erhalten wie in Abbildung 3- 1 0, dann müsste logischerweise jetzt auch einer der drei E2E-Tests »grüngrün
3.7
Der erste Service
1 41
Der eigentliche Karma-Aufruf, um die Unit-Tests a uszuführen, un terscheidet sich dabei bis auf die Angabe einer anderen Konfiguration nicht weiter von dem Aufruf für unsere E2E-Tests. karma s tart karma . conf . j s
Wenn wir keinen Fehler gemacht haben, sollte uns Karma das Feedback über die fehlgeschlagenen Unit-Tests geben. Ch rome 30 . 0 . 1 599 : Executed 5 o f 5 ( 5 FA I LED) E RROR ( 0 . 727 secs I 0 . 022 secs)
)Jeben der zusammengefassten Ausgabe bekommen wir zu jedem Test fall auch einen detaillierten Stack Trace und eine Begründung, warum der Testfall feh lgeschlagen ist. Dabei kann es sich um die Verletzung ei ner von uns formulierten Erwartung oder einen tatsächlichen Fehler im Programmablauf h andeln. In unserem Fall sollte jeder der fünf Testfälle aus demselben Grund fehlschlagen. Erro r : Un known prov i de r : BookDataServ i ceProvi der
\\'ie bei dem FormController la utet a uch bei dem NgMode!Con :roller d ie Antwort: mithilfe des name-Attributs. Es gibt jedoch bei dem NgModelController einen kleinen Unterschied. Während uns -\ngularJS bei dem FormController mithilfe des name-Attributs die
Validierung in Kombination mit der ngModei-Direktive Validierungszustand des NgMode/Controllers
1 72
4
Die Anwendung erweitern
Instanz unter der als Wert a ngegebenen Variable ( hier: book Form) in den aktuell gültigen Scope legt, funktioniert dieser Mechanis mus bei dem NgModelController so, dass die Instanz als Eigen schaft innerhalb des übergeordneten FormControllers publiziert wird. Den Namen der Eigenschaft können wir wieder mit dem Attribut wert festlegen ( hier: t i t l e ) . Das bedeutet schließlich, dass wir mit dem Ausdruck boo k Form . t i t l e auf die NgModeiController-Instanz für das Buchtitel-Feld zugreifen können. Somit lassen sich z.B. mit book Form . t i t l e . $d i rty oder boo k Fo rm . t i t l e . $ i nv a l i d die entsprechen den Zustandseigenschaften erfragen. Mithilfe dieser beiden Eigenschaf ten zeigen wir bedingt eine Fehlermeldung an. Dazu verwenden wir erneut die ngShow-Direktive. Die Anforderung, die wir auf d iese Wei se formulieren, lautet: Wenn der Benutzer mit dem Buchtitel-Feld be reits interagiert hat ( boo k Form . t i t l e . $d i rty) und einer der verwende ten Validatoren ( h ier: req u i red) die Eingabe als n icht valide erachtet ( bookForm . t i t l e . $ i nv a l i d ), dann zeige eine entsprechende Fehlermel dung an. H i nweis z u m FormController u n d Ng ModeiController
Äquivalent zu den fünf Zustandseigenschaften $pri s t i ne, $d i rty, $val i d, $ i nval i d u nd $error sorgen der FormController wie auch der NgModeiController dafür, dass für das Formular bzw. seine Komponen ten auch entsprechende CSS-Kiassen gesetzt werden. Wenn z.B. die $di rty- und $va l i d-Eigenschaften den Wert t rue besitzen, dann an no tiert der NgModeiController das jeweilige -Eiement mit den CSS Kiassen ng-di rty und ng-va l i d. Das bedeutet, dass wir für diese CSS-Kiassen entsprechende Re geln definieren können, um dem Benutzer neben der Fehlermeldung ein visuelles Feedback in Bezug auf die Validierung zu geben. Wir werden darauf nicht mehr weiter eingehen. Wenn Sie das BookMonkey-Projekt aus dem offiziellen GitHub-Repository auschecken, werden Sie erken nen, dass wir diese CSS-Regeln beispielhaft in der Datei ma i n . cs s defi niert haben. Diese Regeln sorgen dafür, dass valide Eingaben mit einem grünen Hintergrund im Eingabefeld gekennzeichnet werden, während in valide Eingaben einen roten Hintergrund erhalten.
Wenn wir uns Listing 4- 1 5 anscha uen, können wir erkennen, dass das Eingabefeld für die ISBN ebenfalls mit dem req u i red-Validator annotiert ist. Auch in diesem Fall weisen wir AngularJS an, die NgModeiController-Instanz i nnerhalb der FormController-Instanz zu publizieren. Somit können wir - wie zuvor a uch - mit dem entspre chenden Ausdruck book Form . i sbn . $d i rty && boo k Fo rm . i sbn . $ i nval i d ei ne bedingte Fehlermeldung anzeigen.
4.1
Der Ad m i n i strationsbereich
Admi n i strati onsbere i c h
Neues Buch a n l egen Buch ed i t i eren
[. . .J
B i tte geben S i e ei ne I SBN ei n . [ . . .J
In Listing 4- 1 5 verwenden wir außerdem die ngDisabled-Direktive, um hinterher im Editieren-Modus die ISB nicht mehr verändern zu können. Neben der required-Direktive bringt AngularJS von Haus aus noch weitere Validator-D irektiven mit, die mit Absicht nach den entspre chenden HTML5-Attributen für die native Validierung benannt sind, um einen reibungslosen Umstieg zu ermöglichen. Im Einzelnen gibt es \'alidator-Direktiven für die folgenden Attribute: req u i red - definiert eine Eingabe als obligatorisch pattern
-
Überprüfung der Eingabe anhand eines regulären Aus
drucks mi n - Festlegen eines Minimumwertes für Zahlenwerte max - Festlegen eines Maximumwertes für Zahlenwerte mi n l ength - Festlegen einer Mindestlänge für Eingaben max l ength - Festlegen eines Höchstlänge für Eingaben
.-\ ußerdem stellt das Framework a uch Validator-Direktiven für die neu en HTML5-Eingabefelder bereit.
1 73
Listing 4- 1 5 Nutzung der ngDisab/ed-Direktive
1 74
4
Die Anwendung erweitern
- Überprüfung auf Zahlenwerte - Überprüfung, ob die E ingabe einer URL ent
spricht - Überprüfung, ob die Eingabe emer E
Mai l-Adresse entspricht
Weitere Validatoren
Listing 4- 16 Der min-Validator bei Eingabefeldern für Zahlen
In L isting 4- 1 6 sehen wir den Template-Ausschnitt, der die Elemen te für das Seitenzahl-Eingabefeld definiert. Wir können erkennen, dass wir in diesem Fal l zusätzlich zum req u i red-Validator noch zwei weitere Validatoren verwenden. Das ist zum einen der Validator, der durch das spezielle -Eingabefeld in Kraft tritt, und zum anderen der mi n-Validator, der überprüft, ob der eingegebene Zahlen wert einem Mindestwert entspricht. Das bedeutet also, dass der Aus druck boo k Form . n umPages . $ i nval i d von diesen drei Validatoren abhän gig ist. Admi n i s t ra t i onsbere i c h
Neues Buch anl egen Buch ed i t i eren
[ . . .] < i nput type = 11 n umbe r 11 mi n = 11 1 11 p l acehol der=11 Sei ten . . . 11 n g -model = 11 boo k . numPages 11 name = 11 n umPages 11 requ i red> D a s B u c h m u s s mi n . ei ne Sei te entha l ten . B i tte geben S i e ei ne g ü l t i ge Sei tenzahl ei n . [. . .]
Bedingte Fehlermeldungen mit der $error-Eigenschaft
Wir wollen nun eine allgemeine Fehlermeldung ausgeben, wenn der requ i red- oder n umber- Valida tor verletzt wurde. Falls allerdings der mi n -
4.1
Der Ad m i n i strationsbereich
1 75
\'alidator nicht erfüllt sein sollte, wollen wir stattdessen eine spezielle Fehlermeldung anzeigen, die den Benutzer darauf hinweist, dass er eine höhere Anzahl von Seiten eingeben sol l . Dazu können wir die $erro r Eigenschaft des NgModeiControllers nutzen. Wie bereits erwähnt, referenziert die $error-Eigenschaft ein Feh lerobjekt, dessen Objekteigenschaften die Zeichenketten-Repräsen rarionen der Validararen sind, d ie im aktuellen Zustand die Eingabe al invalide erachten. Im Falle des NgModeiControllers sind die Wer e dieser Objekteigenschaften a llerdings keine Arrays von Formular. omponenten, sondern Boolean-Werte, die angeben, ob der jeweilige \'alidator die Eingabe a ls invalide a nsieht ( t rue) oder nicht ( fa l se). Demnach können wir u nsere spezielle Fehlermeldung abhängig von dem Ausdruck book Form . n umPages . $error . mi n anzeigen bzw. ausblen den. Wir n utzen dazu w ieder das bekannte Muster des gegenseitigen :\usschlusses, indem wir die ngShow- und ngHide-Direktive einsetzen. Admi n i strati ons berei c h
Neues Buch anl egen Buch edi t i eren
[ . . .] < i nput type= " url " pl aceho l der="Websei te des Verl ags . . . " ng -model = " book . publ i s her . u rl " name = " publ i s herUrl " req u i red> Bi tte geben S i e ei ne g ü l t i ge URL ei n . Bi tte geben S i e d i e Websei te des Verl ags ei n .
[. . .]
Ahnlieh sieht es mit dem Eingabefeld aus, in das wir die URL zur Ver lagsseite eingeben (siehe Listing 4- 1 7) . In diesem Fall wollen wir die
Listing 4-17 Die uri-Validator bei URL-Eingabefeldern
1 76
4
Die Anwendung erweitern
spezielle Fehlermeldung a l lerdings abhängig vom url - Val idator a nzei gen. Ansonsten folgt diese Definition demselben Muster wie die Defini tion des Eingabefeldes für die Seitenzahl. Was jetzt noch fehlt, u m das Feature zum Anlegen eines Buches fertigzustellen, ist der Admi n NewBookCtrl -Controller, für den wir bereit die Datei admi n_new_book . j s erstellt haben. In Listing 4- 1 8 können wir nachvollziehen, dass wir uns von AngularJS - wie ü blich - per De pendency Injection den Boo kDataServ i ce übergeben lassen. Diesen Ser vice verwenden wir in der Funktion s ubmi tAct i on ( ) , um das neue Buch Objekt abzuspeichern, wenn der Benutzer auf den Button mit dem La bel » Buch anlegen « geklickt hat. Außerdem rufen wir in dieser Funk tion die goToAdmi nli s tV i ew ( ) -Funktion auf, u m nach dem Speicher vorgang wieder zur Admin-Listenansicht zu gelangen. Hierzu nutzen wir i nnerhalb der goToAdmi nl i stVi ew ( ) -Funktion den bereits bekannten $1 ocat i on-Service. Listing 4- 18 Der AdminNewBookCtri Controller
bmApp . contro l l er ( ' Admi nNewBoo kCtrl ' , funct i on ($scope , $ l ocat i on , BookDataServ i ce ) $scope . book {}; $ scope . su bmi tßtn label = ' Buch a n l egen ' ; =
$scope . su bmi tAct i on = funct i on ( ) { BookDataServ i ce . storeBoo k ( $ s cope . book ) ; goToAdmi n l i s t V i ew ( ) ; }; $ scope . cancel Act i on = funct i on ( ) goToAdmi n l i stV i ew ( ) ; }; var goToAdmi n l i stV i ew = funct i on ( ) { $ l ocat i on . path ( ' /admi n /boo ks ' ) ; }; }); Trick bei der Verarbeitung von Formularen
Der ausschlaggebende Grund dafür, dass wir die submi tAc t i on ( ) Funktion mit so wenigen Zeilen Quellcode i mplementieren können, ist die Tatsache, dass wir in unserem Template mit der Formularde finition in den ngMode/-Direktiven die Zwei-Wege-Datenbindung zu den Objekteigenschaften des book -Objekts herstellen. Das führt nämlich dazu, dass sich das book-Objekt immer weiter mit den nötigen Infor mationen füllt, während der Benutzer da Formular ausfüllt. Schließ l ich befinden sich also alle Buch-Informationen in dem book -Objekt und somit kann das Objekt unmittelbar der storeBoo k ( ) -Funktion de BookDataServ i ce übergeben werden. Eigentlich müssen wir das book,
4.1
Der Ad m i n i strationsbereich
1 77
bjekt im Scope noch nicht einmal initialisieren ($scope . book = { } ) . �\ngularJS würde die Initialisierung für uns übernehmen, sobald der Benutzer mit der ersten Formular-Komponente interagiert. Allerdings ..., i br es viele 3 rd-Party-Direktiven, die mit nicht i n itialisierten Objekten mcht u mgehen können. Deswegen sollten wir die Initialisierung den noch in d ie eigene Hand nehmen. H i nweis zur Datenbindung mit der n g Modei - D i re ktive
Wir empfehlen bei der Verwendung der ngModei-Direktive immer einen Punkt im Ausdruck zu haben (z.B. n g-mode 1 =" boo k . t i t 1 e " ) . Einerseits hat das den Vorteil, dass wir auf diese Weise sehr einfach ein Objekt (z.B. book) sukzessiv mit den entsprechenden Daten befüllen können (vgl. Template für das Formular zum Anlegen/Editieren eines Buches). Andere rseits können wir damit unangenehme Seiteneffekte vermeiden, die aufgrund der prototypischen Vererbung von Scopes entstehen.
\\'enn wir bis hierhin keinen Fehler gemacht haben, dann sollte es nun möglich sein, neue Bücher anzulegen. Das Formu lar sollte dabei in etwa o aussehen wie in Abbildung 4-4. 0 0
0 BookMo n key c
local host 8080/#/ad m i n / books/n ew
BookMonkey Administrationsbereich �eues B uch anlegen
Abbrechen
X
Buth
ar
egE''l
Abb. 4-4 Ausgabe der Ansicht mit dem Formular zum A nlegen eines neuen Buches in Chrome
1 78
4
Die Anwendung erweitern
Wenn wir mit dem Formular interagieren, können wir feststellen, dass a uch die von uns definierten Validatoren greifen und in den invaliden Zuständen somit die angegebenen Fehlermeldungen angezeigt werden. 4.1 .5 Direktes Feedback während der Eingabe
Abb. 4-5 Skizze der Ansicht zum Anlegen eines neuen Buches mit Vorschau
Tem plates mit der ngl nclude-Direktive einbinden
Nachdem wir das Formular zum Anlegen/Editieren eines Buches er stellt haben, wollen wir uns nun der Vorschau widmen. Der Benutzer soll beim Eingeben der Buch-Informationen ein unmittelbares Feedback dazu erhalten, wie die entsprechende Detailansicht aussehen wird ( vgl. Abbildung 4-5 ) . BookMonkey Administrationsbereich Neues Buch anlegen
I JavaScript für Enterpri. . . Professionell programi. . .
� I
978-3-89864-72
[ [ �e
JavaScript ist längst ni...
=:J
302
r Ochs
�
I
dpu nkt.verlag
G,
up://www.dpunkt.de Abbrechen
J
Vorschau JavaScript für Enterprise-Entvvickler Professionell programmieren im Browser und auf dem Server ISBN: 978-3-89864-728-1 Pages: 302 Author: Oliver Ochs Publisher: dpunkt.verlag
IJl
JavaScript ist längst nicht mehr nur für klassische Webprogrammierer interessant.
Buch anlegen
Durch die Zwei-Wege-Datenbindung lässt sieb das Vorschau-Feature verhältnismäßig einfach rea l isieren, indem wir die Scope-Variablen, zu denen wir mit der ngMode/-Direktive eine Datenbindung herstel len, an einer anderen Stelle im Template ausgeben. Wenn wir auf da Schnellstart-Kapitel ( vgl. Kapitel l ) zurückblicken, dann sollten wir er kennen, dass wir nach diesem Muster bereits zwei k leine Anwendungs beispiele mit Vorscha u gesehen haben. Zum einen h aben wir bei dem einfachen Beispiel mit dem -Feld den eingegebenen Wert mithil fe einer Expression unmittelbar wieder a usgegeben ( vgl. Abschnitt 1 . 1 ). Aber auch unser Color-Picker folgte genau diesem Muster ( vgl. Kapi tel 2.2. 8 ) . Das bedeutet, dass wir a uch hier dieses Muster nutzen könn ten, um das Vorschau-Feature zu implementieren.
4.1
Der Admin istrationsbereich
Allerdings können wir uns einen Großteil des Template-Codes spa ren, wenn wir das Template für die Detailansicht an dieser Stelle wie derverwenden. Schließlich haben wir dort bereits festgelegt, wie die De railansicht aussehen soll . Ausschlaggebend für diese Art der Wiederver wendung ist die sogenannte nglnclude-Direktive. Admi n i strati onsbere i c h
< h 3 ng-hi de= " i s Edi tMode " >Neues Buch anl egen Buch ed i t i eren
1 79
Wiederverwendung eines Templates mit der nglnc/ude-Direktive
Listing 4- 1 9 Die nglnc/ude-Direktive
[. . .J
< d i v>
Vorschau
< d i v>
''ie wir in Listing 4- 1 9 nachvollziehen können, können wir mithilfe der nglnclude-Direktive i n ein Tempiare ein anderes Template einfü.._en . Dazu müssen wir bei der Verwendung dieser Direktive den relati' en Pfad zu dem Tempiare angeben, das an der entsprechenden Stelle emgefügt werden soll. Wir müssen dabei beachten, dass die nglncludeDirektive erwartet, dass der relative Pfad mittels einer Scope-Variable .wgegeben wird. Statische Pfade müssen daher - wie hier - in einfachen \nführungszeichen übergeben werden. H i nweis zu u n serem Vorschau-Feature
Damit die Vorschau rechts neben unserem Formular gerendert wird und einen Rahmen besitzt, haben wir die entsprechenden HTML-Eiemente mit einigen CSS-Kiassen ( sp l i t - s c reen, s i mp l e-border und padded ) an notiert. Auf die genauen CSS-Regeln werden wir hier nicht weiter einge hen. I nteressierte finden den entsprechenden Stylesheet in dem offiziel len GitHub-Repository zum Buch.
\\'enn wir a uch bei diesem Schritt keinen Fehler gemacht haben, sollte die Ausgabe im Browser in etwa so aussehen wie in Abbildu ng 4-6.
Ein Template in einem anderen Template einbinden
1 80
4
Abb. 4-6 Ausgabe derAnsicht mit dem Formular zum Anlegen eines neuen Buches in Chrome (inklusive Live-Vorschau)
Die Anwendung erweitern
0 0
X
� BookMonkey c
localhost 8080/#/ad m i n / books / new
BookMonkey Administrationsbereich Neues Buch anlegen
Vorschau
JavaScript effektiv Wollen Sie javaScript wirklich beherrschen?
68 Dinge, d ie ein guter Ja vaScript-Entwickler wissen sollte • •
Abbrechen
Buch anlegen
• •
ISBN: 978-3-86490- 1 27-0 Seiten: 240 Autor: David Herman Verlag: dpunkt.verlag
Wollen Sie JavaScript wirklich beherrschen?
Voraussicht zahlt sich aus!
Wäh rend wir i m Formular unsere Eingaben tätigen, sollten wir rechts in der Vorschau unmittelbar die getätigten Eingaben erkennen können. Ganz maßgeblich dafür, dass wir dieses Feature so einfach realisieren konnten, ist die Tatsache, dass wir uns im Vorfeld darüber Gedanken gemacht haben, wie wir die Scope-Variablen benennen. Dadurch dass wir im Template für die Detailansicht a uch mit einem book-Objekt ar beiten und konsequent darauf geachtet haben, dass alle Objekteigen schaften ( t i t l e, subt i t l e usw . ) durchgängig identisch benannt sind, können wir das Template ohne Probleme wiederverwenden.
4.1
4.1 .6
Der Adm i n i strationsbereich
1 81
Die Funktion zum Ed itiere n eines Buches
Unsere BookMonkey-Anwendung soll dem Admin a uch eine Möglich keit bieten, bestehende Bücher zu editieren. In der Admin-Listenansicht haben wir dazu bereits den entsprechenden Hyperlink eingefügt, der zu dem Formular zum Editieren eines Buches führt. Somit müssen wir erneut u nsere Routenkonfiguration i n der app . j s aktualisieren und die Route ladmi nlboo k s l : i sbnled i t einfügen ( vgl. Listing 4-20) . Wenn wir diese Route in unserer Applikation öffnen, sollen das Template aus der Datei boo k_form . html und der Admi n Ed i t BookCtrl -Controller geladen werden. Das bedeutet, dass wir bei der Editieren-Funktion das Templa te für das Formular zum Anlegen eines neuen Buches wiederverwenden. Die nötigen Vorkehrungen haben wir i n dem Template bereits getroffen ( vgl. Abschnitt 4. 1 . 4 ) . v a r bmApp
=
angul a r . modu l e ( ' bmApp ' , [ ' ngRoute ' ] ) ;
bmApp . confi g ( funct i on ( $ routeProvi der ) {
Listing 4-20 Routen-Konfiguration für das Formular zum Editieren eines Buches
[. . .] I * Admi n routes * I [. . .] . when ( ' ladmi nlbooksl : i sbnledi t ' , templ ateUrl : ' temp l ates ladmi n lbook_form . html ' , cont rol l er : ' Admi n Ed i t BookCtrl ' }) [. . .] });
Für den Admi nEd i tBookCtrl -Controller müssen wir in unserem Verzeich nis lapplsc ri ptslcontrol l ersl noch die Datei admi n_ed i t_boo k . j s erstel len. Die Implementierung sehen wir in Listing 4-2 1 . bmApp . cont ro 1 1 er ( ' Admi n Ed i t Boo kCtrl ' , functi on ( $ s cope , $routeParams , $ l ocat i on , BookDataServ i ce ) $scope . i sEdi tMode t ru e ; $scope . s ubmi tBtn label = ' Buch edi t i eren ' ; =
var i s bn = $routePa rams . i s bn ; $scope . book = Book DataServ i ce . getBookBy l s bn ( i sbn) ;
Listing 4-2 1 Der AdminEditßookCtri Controller
1 82
4
Die Anwendung erweitern
$s cope . s u bmi tAct i on funct i on ( ) { Book0ataServ i ce . u pdateBoo k ($scope . boo k ) ; goToAdmi n l i s t V i ew ( ) ; }; =
$scope . cancel Act i on functi on ( ) goToAdmi n L i st V i ew ( ) ; }; =
var goToAdmi n l i s t V i ew funct i on ( ) { $ l ocat i on . path ( ' /admi n/books ' ) ; }; =
});
Prinzipiell ist der Admi n Ed i t BookCtrl - Cont ro l l er nach dem gleichen Muster aufgebaut wie der Admi n NewBookCtrl - Controller Zu den we sentl ichen Unterschieden gehört die Definition der Scope-Variable i s Edi tMode. D iese Variable setzen wir auf t rue, um im Tempiare die nötigen HTML-Elemente anzuzeigen bzw. a uszublenden und da Feld für die ISBN zu deaktivieren. Weiterhin weisen wir der Scope Variable submi tßt n label die Zeichenkette » Buch editieren« zu, um die Beschriftung des Buttons, der bei einem Klick die s ubmi tActi an ( ) Funktion aufruft, entsprechend festzulegen. Wir müssen außerdem wissen, welches Buch editiert werden soll. Daher greifen wir er neut mithilfe des $ routePa rams-Service auf die ISBN zu, die per URLPfadparameter übergeben wird. Mithilfe der ISBN holen wir uns mittel Boo kDataServ i ce . getBookBy i sbn ( ) das entsprechende Buch-Objekt und referenzieren es mit der Scope-Variable book . Auf diese Weise stellen wir eine Vorbelegung unseres Formulars sicher. Schließlich müssen wir beim Aufruf der s ubmi tAct i on ( ) -Funktion das Buch-Objekt mithilfe von BookDataServ i ce . updateßook ( ) aktualisieren. Das ist an dieser Stelle al les, um das Editieren-Feature zu implementieren. Das Resultat können wir in Abbildung 4-7 erkennen. .
Vorbelegung des Formulars
4.1 .7
Die Funktion zum Löschen eines Buches
Neben dem Anlegen und Editieren von Büchern ist natürlich a uch da Löschen für einen Admin eine wichtige Funktion. Die eigentliche An sicht ist dabei recht einfach, denn sie besteht aus einer Frage und zwei Schaltflächen, eine zum Bestätigen und eine zum Abbrechen ( vgl. Ab bildung 4-8 ) . Auch für d i e Löschfunktion haben w i r bereits in der Admin Listenansicht einen entsprechenden Hyperlink gesetzt. Somit müs sen wir unsere app . j s um die dort verwendete Route erweitern
4.1
IZJ sookMonkey
() 0
c
Der Ad m i n i strati onsbereich
X
local host 8080 /#/ad m i n / books/ 9 78- .
. .
0
....
®
BookMonkey
_
1 83
Abb. 4-7 Ausgabe der Ansicht mit dem Formular zum Editieren eines Buches in Chrome (inklusive Live-Vorschau)
Administrationsbereich Vorschau
Buch editieren
)avaScript effektiv 6 8 Dinge, die ein gutE
JavaScript effektiv
978 3 86490- 12 7-0
Wollen Sie JavaScript wirklich beherrschen�
68 Dinge, d ie ein guter J avaScript-Entwickler
240
wissen sollte
David Herman dpunkt.verlag http://dpunkt.de/ Abbrechen Buch editieren
• • • •
ISB N : 978-3-86490- 1 27-0 Seiten: 240 Autor: David Herman Verlag: dpunkt.verlag
Wollen Sie JavaScript wirklich beherrschen?
BookMonkey Adm i nistrationsbereich Soll das Buch "Node.js
& Co." wirklich gelöscht
werden?
[
löschen
[ Abbrechen
Abb. 4-8 Skizze der Ansicht zum Löschen eines Buches
1 84
4
Die Anwendung erweitern
( vgl. Listing 4-22 ) . Wenn wir also in unserer Applikation die Rou te ladmi nlbooksl : i s bnlde l ete aufrufen, dann sollen das Template book del ete . html und der Admi nDe l eteßookCtrl -Contro ller geladen wer den. Listing 4-22 Routen-Konfiguration für den Dialog zum Löschen eines Buches
var bmApp = angu l a r . modu l e ( ' bmApp ' , [ ' ngRoute ' ] ) ; bmApp . con f i g ( fu nct i on ( $ routeProvi der) { [ 0 0 0] I * Admi n routes [ 0 0]
*
I
•
. when ( ' ladmi n lbooks l : i s bn ldel ete ' , temp l ateUrl : ' templ ates ladmi n lbook_del ete . html ' , contro l l er : ' Admi n Del eteßookCtrl ' }) [ 0 0 0] });
Wie zuvor auch müssen w i r dafür die entsprechenden Dateien erzeugen. Für das Template erstellen wir in dem Verzeichnis lappltemp l atesladmi nl die Datei boo k_del ete . html . Den Inhalt der Datei können wir in Listing 4-23 erkennen. Listing 4-23 Das Template für den Dialog zum Löschen eines Buches
Admi n i s t rat i ons berei eh
Sol l das Buch " ; { { book . t i t l e } } " ; w i rkl i ch gel öscht werden?
Lösc hen Abbrechen
In dem Template nutzen wir nur Mechanismen, die wir bereits bespro chen haben. Wir stellen dem Admin die Frage, ob das Buch wirklieb gelöscht werden soll, und erwarten, dass er den Löschvorgang bestätigt oder abbricht. Für die beiden Aktionen definieren wir jeweils einen But ton. Wenn der Admin den Löschvorgang bestätigt, dann rufen wir die Funktion del eteßook () auf und übergeben die ISBN. Bei einem Abbruch wird die Funktion cancel ( ) aufgerufen. Auch der Admi nDe l eteßookCt r 1 -Controller besteht aus emer ü bersichtlichen Anzahl von Codezeilen. Wir erstellen für den Con-
4.1
Der Ad m i n i strationsbereich
1 85
rroller die Datei admi n_del ete_boo k . j s im bekannten Verzeichnis 'appjs c ri pts/control l ersj. Wie alle anderen JavaScript-Dateien unse rer Anwendung a uch müssen wir die admi n_del ete_boo k . j s mit einem -Tag in u nserer i ndex . h tml einbinden. bmApp . contro l l er ( ' Admi nDe l eteBookCtrl ' , functi on ( $ scope , $ routePa rams , $ l ocat i on , BookDataServ i ce ) v a r i s bn $ routePa rams . i sbn ; $scope . book BookDataServ i ce . getBookBy l s bn ( i s bn) ; =
=
$ scope . de l eteBook funct i on ( i sbn ) { Boo kDataServ i ce . de l eteBookBy l sbn ( i sbn) ; goToAdmi nli st V i ew ( ) ; }; =
$scope . cance l funct i on ( ) goToAdmi nli s t V i ew ( ) ; }; =
var goToAdmi nli s t V i ew functi on ( ) { $ l ocat i on . path ( ' /admi n/book s ' ) ; }; =
);
In Listing 4-24 sehen wir die Implementierung des Admi n Del eteBookCt rl Lontrollers. Wie beim Admi n Ed i t Boo kCtrl -Controller lesen wir m it f-tilfe des $routePa rams-Service die I SBN des Buches a us, das geehr werden soll . Wir übergeben die ISBN der Servicefunktion 3ookDat aServ i c e . getBookBy l sbn () und referenzieren das zurückgegebe '1e Buch-Objekt mithilfe der Scope-Variable boo k, damit wir im Tem "'late auf den Buchtitel und die ISBN zugreifen können. Zu gu ·er Letzt definieren wir die beiden Funktionen del eteBook ( ) und :ancel ( ) , die aufgerufen werden, wenn wir auf den entsprechenden Button klicken. Die Funktion de l eteBook ( ) ruft intern die Funktion 3ookDataServi c e . de l e teBoo k Byl sbn () auf, um das Buch tatsächlich zu löhen, wenn der Admin den Löschvorgang bestätigt hat. Anschl ießend \ erden wir mithilfe des $1 ocati an- Service zur Admin-Listenansicht wei ·ergeleitet. Letztere Aktion erfolgt a uch im Falle eines Abbruchs. In Abbildung 4-9 können wir uns a nschauen, wie der Löschdialog Ju sieht. Um ihn a ufzurufen, müssen wir in der Admin-Listenansicht Juf den Hyperlink zum Löschen des entsprechenden Buches klicken . .: m Klick auf » Löschen > Digest already in progress« -Fehler entgegenzuwirken .
Implementierung der
bmApp . di rect i ve ( 1 tokenfi e 1 d 1 , functi on ( BookDataServi ce ) { return { [. . .] l i n k : funct i on ( s cope , e l em) { [ . ]
Listing 4-35 Oie tokenfield-Direktive
.
.
func t i on i n i t ( ) { i f ( angu l a r . i sDefi ned ( scope . tokenfi el d . tags ) ) i f ( s cope . tokenfi e l d . tags . l ength > 0 ) { II t h i s c a l l emi t s an 1 a fterCreateToken 1 event II and t h i s wou l d i mpl y a 1 d i gest a l ready II i n progreS S 1 wi thout the i n i t i a l i z ed fl ag el em . tokenfi e l d ( 1 setTokens 1 , s cope . tokenfi e l d . tags ) ;
e l se { scope . t o kenfi el d . tags
i n i t i a l i zed
[] ;
true ;
i ni t () ;
}) ;
Der Hintergrund ist, dass unsere tokenfield-Direktive mit zwei Fällen ordentlich u mgehen muss, denn es gibt zwei potenzielle Aufrufszenari os. Einerseits werden wir die Direktive während des Editierens eines Bu ches verwenden. Andererseits kommt sie ebenfalls zum Einsatz, wenn wir ein neues Buch erstellen. In dem ersten Fall ist es sehr wahrschein l ich, dass das Buch-Objekt zuvor bereits mit bestimmten Tags annotiert wurde. Das bedeutet, dass wir in d iesem Fall dafür sorgen müssen, dass das Tokenfield-Plugin mit den bestehenden Tags vorbelegt wird, damit es die entsprechenden Tokens erzeugen kann. Der Aufruf sieht folgen dermaßen aus: el em . tokenfi el d ( 1 setTokens 1 , s cope . tokenfi el d . tags) ;
init()-Funktion
206
4
Die Anwendung erweitern
Die Auswirkung dieses Aufrufs ist aber der G rund dafür, dass wir den » Digest already in progress« -Fehler erhalten würden, wenn wir nicht mit dem i n i t i al i z ed -Flag arbeiten würden. Denn das Vorbele gen des Tokenfield-Plugins mit diesem Aufruf führt a uch dazu, dass das Plugin für jedes erstellte Token das afterC reateToken-Event a ussen det. Und wir erinnern uns: Als Reaktion auf dieses Event rufen wir die addToken ( ) -Funktion auf, die intern mittels s cope . $appl y ( ) ein manu elles Dirty Checking anstößt. Das bedeutet, dass wir während eines D irty-Checking-Zyklus ( Link-Funktion ) ein weiteres D irty Checking anstoßen und somit den erwähnten Fehler erha lten würden. I ndem wir das i n i t i a l i z ed -Flag erst auf t rue setzen, nachdem wir die potenzielle Vorbelegung durchgeführt haben, ist dieser Fehler nun ausgeschlossen. Listing 4-36 Nutzung der tokenfie/d-Direktive in dem Template mit dem Formular zum Anlegen!Editieren eines Buches
Admi ni strat i ons berei eh
[. . .]
[ ] . . .
[. .]
Vorschau
.
Das ist an dieser Stelle alles, u m das Tokenfield-Plugin rei bungslos in die Welt von AngularJ S einzubinden . Wenn wir unsere tokenfield Direktive nun entsprechend u nserer Defin ition in dem Tempiare mit dem Formular zum Anlegen/Editieren eines Buches { book_form . html ) verwenden ( vgl. Listing 4-36), dann sollten wir nun ein Token Eingabefeld sehen (vgl. Abbildung 4- 1 1 ), das sich nahtlos in den Zy k l us der Zwei-Wege-Datenbindung integriert. Mit d iesem Eingabefeld können wir nun sehr bequem ein Buch in Kategorien eintei len, i ndem wir es mit dem entsprechenden Tag versehen. Außerdem sollten die de finierten Unit-Tests nun a uch » grün « werden. Noch deutlicher wird das Ergebnis, wenn wir im nächsten Unterka pitel die tags-Direktive zum Ausgeben von Tags i mplementiert haben.
4.2
�BookMonkey
0 0 0
c
Kategorisierung d u rch Tag s
X
localhost 8080/#/ad m i n / boo k s / 9 7 8 - 3 -8 64 . .
0
"
0
_
BookM on key Ad min istrationsbereich B u c h ed itieren
Vorschau
CoffeeScript Einfach JavaScript
978-3-86490-050-1 CoffeeScript ist eine junge, kleine
200
CoffeeScri pt Ei nfach J avaScript •
ISBN:
978-3-86490-050-1
Seiten:
Andreas Schubert dpunkt.verlag
200
•
Autor: Andreas Schubert
•
Verlag: dr unkt.verlag
http://dpunkt.de/ coffeescript Abbrechen
web Buch editieren
CoffeeSc ript ist eine junge, kleine Programmiersprache, die nach JavaScript übersetzt wird. Zurück
Denn dann werden sich die Auswirkungen unserer Tag-Eingaben un mittelbar in der Vorschau widerspiegeln. 4.2.4
Die Tags-Direktive: Tags a nzeigen
Neben einer Eingabemöglichkeit für Tags müssen wir natürlich auch einen Mechanismus erstellen, der die eingegebenen Tags wieder aus geben kann. Wie bereits erwähnt, wollen wir auch dazu wieder eine Direktive implementieren. Die tags-Direktive soll ein neues HTML-Element definieren. Weiterhin soll es möglich sein, über ein HTML-Attribut tag-data eine Datenquelle für die Tag-Ausgabe festzulegen. Bei der Datenquelle wol len wir uns auf ein Array von Zeichenketten einigen . Listing 4-37 zeigt das Template für die Detailansicht eines Buches ( book_detai 1 s . h tml ) mit der Verwendung unserer tags-Direktive. Das tag-data-Attribut setzen
207
Abb. 4- 1 1 Ausgabe des Formulars zum Anlegen!Editieren eines Buches mit tokenfie/d-Oirektive in Chrome
208
4
Die Anwendung erweitern
wir auf boo k . tags, um der Direktive das Array mit den Buch-Tags zu übergeben. Listing 4-37 Nutzung der tags-Direktive in der Detailansicht
Die Implementierung der tags-Direktive
Listing 4-38 Die tags-Direktive
[ . . .]
Z u rück
11
Wie wir uns sicherlich vorstellen können, ist die Implementierung unserer tags-Direktive recht übersichtlich. Dafür erstellen wir zunächst in dem Verzeichnis /app/scri pts/di rect i ves/ die Datei tags . j s, in der wir die Direktive implementieren werden. Den Inhalt dieser Datei sehen wir in Listing 4-3 8 . bmApp . d i rect i ve ( 1 tags 1 , funct i on ( ) { retu rn { restri ct : 1 E 1 , t emp l ateUrl : 1 COmponent_templ ates/di rect i ves /tags . html 1 , s cope : { tagData : = 1 1
}) ;
Auch hier definieren wir die Direktive mithilfe eines Direktiven Definitions-Objekts (DDO). Die re stri ct E igenschaft setzen wir auf die Zeichenkette 1 E 1 , um AngularJS mitzuteilen, dass wir mit dieser Definition ein HTML-Eiement definieren wollen . Außerdem legen wir mithilfe der temp l ateUrl -Eigenschaft fest, dass u nsere Direktive ein eige nes Tempiare besitzen soll. Weiterhin müssen wir der scope-Eigenschaft ein Objekt mit der tagData -Eigenschaft zuweisen. Mit dieser Eigen schaft ermöglichen wir unserer Direktive den Zugriff auf das tag-data Attribut. Das Ganze entspricht den bekannten Namenskonventionen ( vgl. Abschnitt 2.2. 8 ) . Dadurch dass wir eine Zwei-Wege-gebundene Referenz auf das übergebene Array erhalten wollen, legen wir für die tagData -Eigen schaft den Wert = fest. Wir werden sehen, dass diese -
1
1
4.2
Kategorisierung durch Tags
Zwei-Wege-Verbindung a usschlaggebend dafür ist, dass sich die Tag Ausgabe unserer Direktive automatisch aktualisiert, wenn sich der In halt des Arrays ändert. Nun müssen wir noch das Template definieren. Dazu erste!len wir im app-Verzeichnis u nserer Anwendu ng das Verzeichnis component_templ ates. In diesem Verzeichnis erstellen wir außerdem noch das Unterverzeichnis di rec t i ves. Dort legen wir schließlich die Datei tag s . html an. Das ha ben wir in Listing 4-38 mit der temp l ateUrl Eigenschaft so definiert. Das Tempiare unserer tags-Direktive sehen wir in Listing 4-39. Für die Ausgabe des Zwei-Wege-gebundenen Arrays s cope . tagData nutzen wir einen alten Bekannten: die ngRepeatDirektive. Die Schleifenvariable tag geben wir mit einer gewöhnlichen Expression { { tag } } aus und sorgen mit der CSS-Kiasse bm- t ag dafür, dass unsere Tags einen blauen Hintergrund und a bgerundete Ecken erhalten. Auf die genaue CSS-Regel gehen wir hier nicht weiter ein. { { tag } }
Da die ngRepeat-Direktive für jedes Element aus dem Array a utoma tisch das entsprechende -Element erzeugt und die Ausgabe im Falle einer Array-Manipulation ebenfalls automatisch aktualisiert, wer den wir nun in der Vorscha u des Formulars für das Anlegen/Editieren eines Buches ein direktes Feedback erhalten, wenn wir mithilfe der to kenfield-Direktive neue Tags einpflegen bzw. entfernen. Wir erinnern uns: In dem Tempiare für das Formular zum Anlegen/Editieren eines Buches verwenden wir das Tempiare für die Detaila nsicht wieder, um die Live-Vorschau zu erstellen. Das ist alles, um eine Möglichkeit zur Ausgabe von Tags zu schaf fen. Wenn wir keinen Fehler gemacht haben, sollte die Ansicht zum Anlegen/Editieren eines Buches nun so a ussehen wie Abbildung 4 - 1 2. Wenn wir jetzt Tags hinzufügen oder entfernen, dann wird sich a uch die Live-Vorschau entsprechend aktualisieren. Zusammenfassung
In Unit-Tests werden die internen Mechan ismen von AngularJ S n icht automatisch a usgeführt, um dem Entwickler d i e volle Kon trolle über den Testablauf zu geben. Das bedeutet, dass wir beim Testen von Direktiven einige D inge manuell erledigen müssen, die das Framework bei der normalen Ausführung automatisch durchführen würde.
209
Das Template für die tags-Direktive
Ausgabe der Tags mithilfe der ngRepeat-Direktive
Listing 4-39 Das Template der tags-Direktive
210
Abb. 4- 12 Ausgabe der Live-Vorschau des Formulars zum Anlegen!Editieren eines Buches mit tags-Direktive in Chrome
4
Die Anwendung erweitern
() ()
IZJ sookMonkey
X
x
localhost:4730/api/books
local host 8080/#/ad m i n / books / 9 78 - 3 - 86 4
0
�) -
BookMon key Ad m i n i strationsbereich B uch ed itieren
978-3-86490-050-1 CoffeeScript ist eine junge, kleine
CoffeeSc ript Ei nfach JavaScri pt ISBN:
200
978-3-86490-050-1
Seiten: 200
Andreas Schubert
•
dpunkt. verlag
Autor: Andreas Schubert Verlag:
J u
http://dpunkt.de/ coffeescript Abbrechen
web Buch editieren
CoffeeScript ist eine junge, kleine Programmiersprache, die nach JavaScript übersetzt wird.
So müssen wir insbesondere das Tempiare manuell kompilieren, in dem die Direktive verwendet wird. Dazu nutzen wir den $compi 1 e Service. Beim Kompilieren wird AngularJS die zu testende Direktive erkennen und somit unter anderem die Link-Funktion der Direktive a usführen. Wir kompilieren ein Template immer gegen einen Scope. In Unit Tests können wir einen neuen Scope erzeugen, indem wir uns von AngularJS den $ rootScope per Dependency Injection übergeben las sen und auf dem $ rootScope die $new ( ) -Funktion aufrufen. In Unit-Tests für Direktiven müssen wir oftmals die genaue Be schaffenheit des DOM überprüfen. Über angul a r . el ement ( ) können wir dazu jqLite nutzen . Das ist die eigene j Query-Implementierung, die AngularJS mitbringt. Die Einschränkung ist jedoch, dass jq-
4.3
21 1
Einen REST Web Service a n b i nden
Lite nur eine Untermenge der Funktionen der richtigen jQuery lmplementierung bereitstellt. Falls wir in unserer i ndex . h tml die richtige j Query-lmplementierung vor AngularJS importiert haben, wird AngularJS das feststellen und jeden Aufruf von ang u l a r . el ement ( ) an die richtige Implementie rung weiterdelegieren. Mithi lfe von Direktiven ist es ziemlich einfach, bestehende j Query-Plugins und andere 3 rd-Party-Bibliotheken in die Welt von AngularJS einzubinden. Jedoch müssen wir beachten, dass wir ein Plugin rei bungslos in den Zyklus der Zwei-Wege-Datenbindung integrieren. Dazu müssen wir bei jeder Scope-Manipulation, die i nnerhalb einer Callback-Funktion durchgeführt wird, die innerhalb einer 3 rd Party-Bibliothek zur Ausführung kommt, dafür sorgen, dass wir mittels scope . $appl y ( ) das D i rty Checking manuell anstoßen.
4.3
Einen R EST Web Service a n bi nden
Wenn wir ehrlich sind, ist unsere Anwendung bisher noch ziemlich langweilig, weil jeder Benutzer seinen Datenausschnitt im eigenen Browser besitzt u nd es keine übergeordnete Datenverwaltung gibt. Das bedeutet, dass es i nsbesondere n icht möglich ist, Buchinformationen zwischen zwei laufenden Anwendungen zu synchronisieren und auf den jeweils anderen Datenausschnitt zuzugreifen. Diese Einschränkung wollen wir in diesem Kapitel aufheben, indem wir u nsere Appl ikationsdaten zentral auf einem Server verwalten. Die Kommunikation mit einem Backend gehört dabei zu den typischen Anwendungsfällen bei Single-Page-Anwendungen. Wie wir beim Projektstart bereits erwäh nt haben, nutzen wir dazu eine Backend-Implementierung, die wir auf Basis von Node.js erstellt haben. Mithilfe des Express-Frameworks6 stellen wir für unsere AngularJS-Anwendung einen Endpunkt zur Verfügung, auf den u nsere Applikation über HTTP zugreifen kann. Dabei haben wir versucht, die Backend-Schnittstelle - soweit es für unser Beispiel praktika bel ist möglichst >> R ESTful « zu gestalten. Somit stellt unser Backend einen sogenannten R EST Web Service bereit. Auch in diesem Schritt definieren wir die Anforderung mithilfe einer User Story. 6 http://expressjs.com/
Kommunikation mit einem Backend Node.js Backend
RESTfu/ Web Service
212
4
Die Anwendung erweitern
Als Nutzer Michael möchte ich meine Daten auf einem zentralen Server speichern, um diese von mehreren Endgeräten aus abrufen zu können.
4.3 . 1
Die REST-Ressourcen
Das BookMonkey-Backend
An dieser Stelle wollen wir das Backend kurz vorstellen. Um den Fo kus weitestgehend a u f die Backend-Schnittstelle zu lenken, werden wir die I mplementierung allerdings n icht im Detai l besprechen. Wer sich mit Node.js und dem Express-Framewerk intensiver auseinandersetzen möchte, dem sei an d ieser Stelle das Buch ,, ode.js und Co. > Post-Build Action« die Option » Publish JUnit Test Result Report« a uswählen. Hier können wir nun unsere Ausgabedatei mit den Testergebnissen aus wählen, die dann nach jedem D u rchlauf eingelesen und angefügt wer den. Zusätzlich können Schwellenwerte definiert werden, die angeben, ab wann der komplette Build als fehlgeschlagen zu werten ist. Zusam menfassung
Karma ist ein Werkzeug, das auf der Node.js-Plattform basiert. Es ermöglicht uns das automatisierte Ausführen von Tests. 28 https://wi k i . j enk ins-ci.orgldisplay/jENKINS/xUnit + Plugin
Listing 5-4 1 Mit Karma einen JUnit-Report generieren
Testergebnisse in Jenkins importieren
270
5
Projektverwaltung und Automatisierung
Hierzu startet Karma einen eigenen Webserver, auf den sich dte Testgeräte verbinden müssen. Somit ist jeder Browser auf jedem Endgerät, der über das Netzwerk auf diesen Server zugreifen kann. ein potenzieller Ort für die Testausführung. Auf diese Weise kann d ie A usführung auch auf mobilen Endgeräten sehr einfach gewähr leistet werden. Die Konfiguration erfolgt über eine einfache Datei tm JSO Format, die standardmäßig karma . conf . j s heißt. Es gibt eine Integration in die WebStorm-IDE. Wir haben die Möglichkeit, verschiedene Test-Frameworks einzu binden, die wir für die Spezifikation unserer Testfälle verwenden können. D ie bekanntesten Frameworks i m JavaScript-Bereich ha ben wir kurz vorgestellt. Kanna bietet uns Mechanismen an, die uns eine einfache Integrati on i n ein CI-System ermöglichen. Wir haben die Möglichkeit, über Plugins einen Export der Ergeb nisse in eine XML-Datei anzustoßen. Diese Datei d ient als Schnitt stelle zu weiteren Systemen. Anhand eines Beispiels haben wir die Integration in ein bestehende System mit Maven und ]enkins dargestellt.
5.5 5.5.1
Umfassende Unterstützung beim Aufbau der Projektstruktur
Yeoman: E i n defi n ierter Workflow Was ist Yeoman ?
Yeoman29 ist ein in der Praxis getesteter Workflow, der es uns erlaubt. sich vollständig auf die Entwicklung der eigentlichen Anwendung zu konzentrieren. Durch eine geschickte Kombination von Generatoren und der Werkzeuge Bower und Grunt kann die Umsetzung des eigent lichen Anwendungsfa l ls fokussiert werden. Begleitende Nebenaufgaben wie das Festlegen einer sinnvollen Ordnerstruktur, die Verwaltung von Abhängigkeit oder das Heraussuchen der genauen Syntax für Anwen dungskomponenten überlassen wir dabei den entsprechenden Werkzeu gen. Auch für Aufgaben, die die Entwicklungsinfrastruktur betreffen, gibt es in Yeoman eine Lösung. Dazu gehört insbesondere die Bereit stellung eines Webservers und die Erzeugung eines Build-Prozesses.
29 http://yeoman.io/
5.5
5.5.2
Yeoman: Ein defi n ierter Workflow
271
Yeoman i nstal lieren
Yeoman ist ein Node.js-Modul. Wir können dieses Werkzeug also mithilfe von npm installieren:
lnstallation über npm
npm i ns t a l l -g yo
Die Installation beinhaltet neben dem Generator Yo die Werkzeuge Bower und Grunt, weil d iese a ls direkte Abhängigkeiten von Yeoman eingetragen sind. Somit sind wir nach der erfolgreichen Installation in der Lage, über die Kommandozeile die Befehle yo, bower und g runt zu nutzen. D ie offizielle I nstallationsanleitung ist ebenfa l ls online verfüg bar30 . Um den Generator Yo tatsächlich nutzen zu können, benötigen wir zunächst noch einen Generator für das Framework, mit dem wir arbeiten wollen. D iese Generatoren sind ebenfa ll s Node.js-Module und können somit a uch über npm installiert werden. Beispielhaft ist an die ser Stelle die Installation des Generators für AngularJS aufgeführt: npm i ns t a l l - g generator-angul a r
E s gibt natürlich noch viele weitere Generatoren, d i e w i r installieren können. Wir haben einerseits die Möglichkeit, ü ber npm direkt nach Generatoren zu suchen. Andererseits können wir uns auch in dem of fiziellen GitHub-Projekt31 die Liste der Generatoren ansehen. Um mit hilfe von npm eine entsprechende Suchanfrage zu starten, können wir den nachfolgenden Befehl verwenden: npm search generator-%name%
5.5.3
Anwendungsbausteinen generieren
Wir haben i n jedem neuen Projekt zunächst die Aufgabe, die grundle genden Strukturen für das Projekt zu erstellen. Dazu gehört bei Weban wendungen in erster Linie die Erstellung von bestimmten Dateien wie z.B. einer i ndex . h tml und üblicherweise Verzeichnissen für Bilder, CSS und JavaScript-Dateien (die sogenannten Assets} . Allerdings ist es so, dass es kein Patentrezept zur Erstellung dieser grundlegenden Struktu ren gibt. Ganz im Gegenteil: Häufig stehen wir als Entwickler vor der Qual der Wahl, die oft durch persönliche Vorlieben beeinflusst wird. Die einen Entwickler führen mit a s sets ein Zwischenverzeichnis ein. Andere nennen dieses Verzeichnis res oder verzichten komplett darauf. Wie dem auch sei, wenn wir ehrlich sind, sind diese wiederkehrenden 30 http://yeoman.io/gettingstarted. html 31 https://girhub.com/yeoman/
Bootstrapping eines Projektes
272
5
Inspiriert durch Rails
Eng/. »Scaffo/ding«
Es gibt verschiedene Generatoren.
Projektverwaltung und Automatisierung
Aufgaben mühselig, fehleranfä llig und führen zu vielen unterschiedli chen Projektstrukturen. Warum können wir diese Aufgaben dann nicht einfach automatisieren und homogenisieren ? Durch Rails i nspiriert, entstand das Projekt Yo32 , das sich der Lösung dieser Aufgabe für clientseitige Webanwendungen widmet. Yo ist ein Kommandozeilenwerkzeug, mit dem wir die Grundstrukturen für neue Projekte automatisch und einheitlich generieren können. Die De finitionen h ierfür werden in sogenannten Generatoren bereitgestellt. Yo bringt bereits Generatoren für viele aktuell relevanten Frameworks mit. Dazu gehört a uch, wie bereits erwähnt, ein Generator für AngularJS33 • Tatsächlich ist der AngularJS-Generator einer der ersten Generatoren, die in diesem Projekt entwickelt wurden. Neben dem Generator für AngularJS sind u nter anderem Generatoren für die folgenden Frame works verfügbar: Ember.js Backbone.js jQuery Jasmine Mocha Twitter Flight Die Benutzung eines Generators hat den positiven Nebeneffekt, da s die Projektstrukturen immer einheitlich sind und sich somit neue Ent wickler in bestehenden Projekten schneller zurechtfinden . 5.5.4
Generierung von AnWendungsbausteinen
Yo für AngularJS-Projekte
Der Generator für AngularJS war einer der ersten Generatoren, die in nerhalb des Yeoman-Projektes entstanden sind. Er bietet uns die Mög lichkeit, die typischen AngularJS-Anwendungsbausteine zu generieren u nd automatisch in unsere i nde x . h tml einzutragen . Außerdem wird für jeden erzeugten Anwendungsbaustein sofort eine Test-Suite erstellt. Wir werden in diesem Kapitel die verschiedenen Sub-Generatoren besprechen . Zunächst schauen wir uns jedoch in Listing 5-42 die grund legende Ordnerstruktur an, die der AngularJS-Generator erstellt. Auf der ersten Ebene haben wir durch die Verzeichnisse app und test eine grundlegende Trennung in Anwendungs- und Testdateien. I nnerhalb 32 http://yeoman . io/ 33 https://github.com/yeoman/generator-angular
5.5
Yeoman: Ein defi n ierter Workflow
273
des app-Verzeichnisses haben wir eine weitere Unterteilung in Skript-, Stylesheet und Template-Dateien. In dem scri pts-Verzeichnis differen ziert der Generator noch einmal zwischen den verschiedenen Anwen dungsbausteinen, die AngularJS mitbringt ( Controller, Services, Direk tiven, usw. ) . app/ scri pts/ cantro l l ers / decorators / d i rec t i ves/ fi l ters/ serv i ces/ s tyl es/ v i ews/ tes t / s pec/ contro l l ers/ decorators / d i rec t i ves/ fi l ters/ serv i ces/
Innerhalb des test-Verzeichnisses gibt es das Unterverzeichnis spec, das die Spezifikation der verschiedenen Anwendungsbausteine unserer Anwendung enthält. Das Zwischenverzeichnis spec existiert aus dem Grund, dass es hierzu para l lel noch ein Verzeichnis e2e geben kann, in dem unsere E2E-Tests definiert werden. Da sich diese Art von Tests a ufgrund ihrer stark fachlich getriebenen Definition nicht so leicht ge nerieren lassen, m üssen wir uns an dieser Stelle selber um die Erstellung kümmern. H inweis zur Modularisierung i n größeren Projekten
Die Verzeichnisstruktur, die der Generator generiert, ist für kleine Projek te oder Module geeignet. in größeren Projekten sollten wir das Modul system von AngularJS intensiv nutzen, um unsere Anwendung zu struk turieren und in fachliche Komponenten zu unterteilen. Leider bietet sich die generierte Verzeichnisstruktur des AngularJS-Generators nur für so genannte Single-Module-Anwendungen an. Es ist jedoch möglich, inner halb von einzelnen Modulen den Generator dennoch einzusetzen. Die Verwaltung von mehreren Modulen kann mit Yo bisher noch nicht au tomatisch durchgeführt werden. Wie wir unsere Anwendungen generell mithilfe von Modulen strukturieren können, besprechen wir in U nterkapi tel 7. 1 .
Listing 5-42 Generierte Verzeichnisstruktur des AngularJS-Generators
274
S
Projektverwaltung und Automatisierung
Der AngularJS-Generator beinhaltet verschiedene automatisierte Rou tinen, die über die Kommandozeile aufgerufen werden können. Wir können dem Werkzeug yo den gewünschten Generator per Parameter übergeben. In u nserem Fall nutzen wir h ierfür den Parameter ang u l ar. yo angul a r Bootstrap eines AngularJS-Projektes
Listing 5-43 Ausschnitt der möglichen AngularJSGenerator-Befehle
Geben w i r keine weitere Spezialisierung a n , legt u n s y o nun d i e grund legende Projektstruktur für eine AngularJS-Anwendung an. Durch einen Doppelpunkt getrennt, können wir einen der vorhandenen Sub Generatoren a uswählen, um spezifische Anwendungsbausteine zu ge nerieren. Möchten wir z.B. einen neuen Controller hinzufügen, können wir yo mit dem Parameter ang u l a r : control l er ausführen. Über einen weiteren Parameter können wir den Name des zu generierenden Con trollers definieren. Insgesamt sind die folgenden Sub-Generatoren ver fügbar: yo yo yo yo yo yo yo yo
angu l a r [app l i ca t i onName] angul a r : contro l l er Con t ro l l erName angul a r : decorator Serv i ceName angul a r : d i rect i ve d i rec t i veName angu l a r : f i l ter fi l terName angul a r : route routeName angu l a r : serv i ce serv i ceName angul a r : v i ew v i ewName
H i nweis zur Nutzung von CoffeeScript
Für Entwickler, die ihre Applikationen lieber in CoffeeScript schreiben, gibt es die Möglichkeit, über den Parameter - -coffee die entsprechen den CoffeeScript-Vorlagen zu nutzen. Außerdem ist es möglich, über den Parameter - -mi nsa fe nur Vorlagen zu benutzen, die auch nach einer Mi nifizierung des Quellcodes weiterhin funktionieren. Das sollte allerdings normalerweise durch den Build-Prozess erledigt werden, sodass wir uns um die Minifizierung während der Entwicklung keine Gedanken machen sollten.
Namen der Komponenten in CameiCase
Eine Besonderheit, die wir bei der Ben utzung des Generators beachten sollten, ist die Namenskonvention von Komponenten. Der per Parame ter übergebene Name für unsere Komponente wird innerhalb der Vor lage mithilfe der Funktion camel i ze ( ) 34 aus der JavaScript-Bibliothek Underscore.js in die CamelCase-Konvention transformiert. Das be deutet, dass die AngularJS-Komponente in unserer Anwendung z.B. 34 https://gi thu b.com/epel i/underscore.string
5.5
Yeoman: Ein defi n ierter Workflow
Mei nCont ro l l erCtrl heißt, wenn wir einen Controller mit dem Komman dozeilenaufruf yo ang u l a r : cont rol l e r mei n -contro l l er generieren. Die erzeugte Datei wird a llerdings den Dateinamen m e i n - contro l l er . j s tra gen und unter appjscri ptsjcontrol l ers/ zu finden sein. Das kann unter Umständen zu Verwirrung führen. Deswegen sollten wir d iese Konven tionen kennen. Neben der Erstellung der gewünschten Komponente kümmert sich der Generator ebenfalls darum, dass die erzeugte Datei automatisch an der korrekten Stelle in unserer i ndex . h tml eingetragen wird. Da AngularJS von Anfang an unter dem Aspekt der Testbarkeit entwickelt wurde, ist es n icht überraschend, dass der Generator uns ebenfalls die benötigten Testdateien für unsere Komponenten generiert. �-------------------�
app(defau lt)
( ( (
) ) )
add common
add main
add test env
!( !(
directive
'
hPok
filler
•
(
.
Automatische Erweiterung der index.html
Abb. S-3 Übersicht über die Bausteine eines Yeoman-Generators
'
controller
service route rewrite app js
•
275
•
•
)
,
� - ------- - - - - - - - - - - - �
)
hook
:i
view
�------------------------------1
, ' ' ' ' '
add template
( : (�============� : ------ - ---- - - -
:
add lest template add to Index
'- - - -
-
-
-
-
- -
-
-
-
---
yo angular
Mithilfe des Befehls yo angu l a r können wir eine neue AngularJS Anwendung erstellen. I ntern wird hierbei der Su b-Generator angu l ar : app aufgerufen, der als Standard definiert ist, falls kein S ub-Generator explizit angegeben wird. Mit einem optionalen Parameter können wir den Name der Anwendung festlegen. Wenn wir den Generator ohne Namen a usführen, wird der Name des aktuellen Ordners a ls Anwendungsname gewählt. In beiden Fällen wird an den Anwendungsnamen die Zeichenkette App angefügt, sodass wir mit dem Parameter UserMan ager eine AngularJ S-Anwendung mit dem Namen UserManagerApp erhalten. Mit der Ausführung dieses Sub-Generators
Sub-Generatoren können übereinen Doppelpunkt angegeben werden.
276
5
Projektverwaltung und Automatisierung
wird eine erste lauffähige Anwendung generiert. Somi t sind unre· anderem die folgenden üblichen Initialisierungsaufgaben erledigt. Grundlegende Ordnerstruktur i ndex . h tml mit bereits eingebundenem AngularJS
Erste Route inklusive Tempiare und Controller Allgemeine Dateien wie ein 404-Template oder eme . htaccess Datei Testumgebung für Unit- und Integrationstests Basiskonfiguration für die Abhängigkeitsverwaltung Darüber hinaus können wir die folgenden Module optional einbinden: Twitter Bootstrap Angular Resource Angular Cookies Angular Sanitize Dazu stellt uns Yo beim Generieren jeweils eine entsprechende Frage, die wir mithilfe einer Eingabeaufforderungen beantworten können. Am Ende stößt das Skript den Befehl npm i n s t a l l an, mit dem die Abhängig keiten für den Build-Prozess installiert werden. Diese sind in der Datei pac kage . j son definiert. Auf d iesen Aspekt werden wir im nächsten Un terkapitel genauer eingehen. H inweis zu Grunt i n nerh a l b von Yeoman
Neben den Projektstrukturen für unser Projekt generiert uns yo auch die Datei Gruntfi l e. j s, die eine Entwicklungs- und Build-Umgebung für un ser Projekt definiert. Auf die Möglichkeiten des Grunt-Servers innerhalb von Yeoman gehen wir am Ende dieses Kapitels noch einmal detaillierter ein.
yo angular:controller
Mit dem Sub-Generator cantro l l er können wir einen neuen Control ler erstellen. Der Name wird wie üblich mith ilfe des ersten Parameter übergeben. Zu beachten ist h ier, dass d ieser Name in der Vorlage ei nerseits durch die Underscore.js-Funktion c l ass i fy () modifiziert und
5.5
Yeoman: E i n defi n ierter Workflow
277
die Zeichenkette >> Ctrl Sch1.bert 97 Ol• Ochs 'i C R.xicn Elements
x
•
Network
aP�" 'b•d.pp''
��� �� -. �
Sry e s
Compured
elefl'ent . style {
}
»
+
0·
> /h · n ·· r us e r &qen t s t y le s he�· dlv { r ji •" • • >-< : • • : AalltnlH nH i on sbe re H h< /h 2>
c�crtpt srcc" scqpts/con t r o t\ers/adlll ln nsw book;, !S"> • •. Y
. . •·
ng-model=
"searchText'' c lass=" n g-scope ng-pristine ng-vat id''> •