Angular 2 Kochbuch
Angular 2 Kochbuch
Nikolas Poniros
Buy on Leanpub

Inhaltsverzeichnis

Einleitung

Angular 2 ist eine von null auf neu geschriebene Version des populären Google Frameworks AngularJS. Die neue Version des Frameworks wurde im März 2014 in einem Blog Artikel angekündigt. Anfangs war es geplant, das Framework in einer neuen Sprache names AtScript zu schreiben. Letztendlich wurde aber entschieden, es in Microsofts TypeScript zu schreiben und es dann in JavaScript zu kompilieren. Wir als Entwickler haben die Möglichkeit, Angular 2 Anwendungen in TypeScript, ECMAScript 6 (ES6) / ECMAScript 2015 (ES2015), ECMAScript 5 (ES5) oder Dart zu schreiben. In diesem Buch werden wir uns auf TypeScript konzentrieren, da diese vermutlich die am häufigsten verwendete Sprache sein wird, um Projekte mit Angular 2 zu schreiben.

Angular 1.x. vs. 2

Wie schon bei der Ankündigung zu lesen war, gibt es teilweise große Unterschiede zwischen Angular 2 und 1.x. Diese Unterschiede haben dafür gesorgt, dass die Community die Entscheidung des Angular-Teams, das Framework neu zu schreiben, mit gemischten Gefühlen aufgenommen hat. Die templating-Sprache wurde geändert und alle Event-Direktiven wie z. B. ng-click und ng-keyup wurden von eine neue Schreibweise für Events ersetzt. Controllers und $scope wurden aus dem Framework entfernt und durch Komponenten ersetzt. Die Definition von Services wurde vereinfacht. In den meisten Fällen ist ein Service einfach eine Klasse. Außer den Unterschieden die uns als Entwickler bei der Implementierung von Angular 2 Anwendungen direkt betreffen wie z. B. die neue Syntax für Templates, gibt es auch jede Menge technische Unterschiede mit denen wir uns in den meisten Fällen nicht beschäftigen brauchen wie z. B. hierarchische Dependency Incjection und hierarchische Change-Detection. Trotz den Unterschieden sind die 1.x. und 2 Versionen von der Vogelperspektive betrachtet sehr ähnlich. Die meiste Begriffen die wir aus Angular 1.x. kennen, können weiterhin verwendet werden.

Ziel des Buches

Ziel des Buches ist es dem/r Leser/in fertige Lösungen für häufige Probleme bereitzustellen, die er/sie mit wenig Aufwand in neue oder existierende Angular 2 Anwendungen einbauen kann. Es ist nicht die Absicht dieses Buches, in die Tiefen von Angular 2 vorzudringen und den/die Leser/in über die gesamte Funktionalität und Implementierung von Angular 2 zu informieren.

Für wen ist dieses Buch

Dieses Buch ist für Entwickler/inen mit wenig oder keiner Angular 2 Erfahrung gedacht, die schnell bestimmte Probleme lösen möchten. Auch wenn wir in den Rezepten mit TypeScript arbeiten, ist TypeScript-Wissen keine Voraussetzung. Dafür gibt es in Kapitel 1 eine kurze Einführung in TypeScript.

Aufbau des Buches

Das Buch ist in mehrere Kapitel unterteilt. Jedes Kapitel beinhaltet ein oder mehrere Rezepte, die Lösungen zu bestimmten Problemen bieten. Der Aufbau eines Rezepts ist wie folgt:

  1. Problem: Für was ist dieses Rezept gut? Was können wir mit diesem Rezept erreichen?
  2. Zutaten: Hier wird alles aufgelistet, was gebraucht wird, um die Lösung(en) zu implementieren
  3. Lösung(en): Eine oder mehrere Lösungsmöglichkeiten für das besagte Problem
  4. Diskussion: Vor-/Nachteile einer Lösung, Probleme auf die man stoßen könnte, etc.
  5. Code: Links zu Github-Repositories mit Beispiel-Code, Links zu Webseiten mit Online-Demos
  6. Weitere Ressourcen: Links zu Webseiten/Teilen des Buches mit tiefer reichenden Informationen zum Thema. Hier geht es hauptsächlich um technische Themen

Die Punkte 1, 2, 3 und 5 sind in jedem Rezept vertreten, die Restlichen nur nach Bedarf.

Überblick

Kapitel 1: Einführung in TypeScript beinhaltet genügend Informationen über TypeScript, damit die Rezepte verständlich ist.

Kapitel 2: Basisrezepte, die für die Lösungen benötigt werden, die in weiteren Kapitel präsentiert werden.

Kapitel 3: Rezepte, um mit der Anzeige zu interagieren beinhaltet Rezepte mit denen wir die Anzeige (View) abhängig von Daten in einer Komponente verändern können.

Kapitel 4: Rezepte für Formulare beinhaltet Rezepte, um Formulare zu bauen. In diesem Kapitel wird auch die Validierung von Nutzer-Input behandelt.

Kapitel 5: Rezepte für den Datenaustausch beinhaltet Lösungen die wir anwenden können, um Daten mit einem Server auszutauschen.

Kapitel 6: Rezepte für Routing enthält Lösungen zu Problemen, die mit dem clientseitigen Routing einer Single Page Anwendung zu tun haben.

Kapitel 7: Rezepte für Komponenten enthält Lösungen zu Problemen, die Komponenten betreffen, wie z. B. Kommunikation zwischen Komponenten.

Kapitel 8: Rezepte für ngFor-Listen enthält Lösungsvorschläge, die uns bei der Arbeit mit Listen helfen können.

Hilfe und Support

Bei Fragen und Anregungen zum Buch und/oder dem Beispiel-Code gibt es die Möglichkeit, Issues bzw. Pull-Request über Github zu öffnen.

Bei allen anderen Fragen und Anregungen steht die E-Mail-Adresse info@angular2kochbuch.de zur Verfügung.

Einführung in TypeScript

Vermutlich ist TypeScript für einige Leser und Leserinnen Neuland. Aus diesem Grund haben wir uns entschieden, dem Buch auch eine kurze Einführung in TypeScript voranzustellen. TypeScript ist eine von Microsoft entwickelte Programmiersprache mit der man Anwendungen schreiben kann, die später zu JavaScript kompiliert werden. Es ist eine typisierte Übermenge von JavaScript. Neben Typen unterstützt TypeScript sowohl gewisse Features aus ES6/ES2015 als auch Features die vermutlich in späteren ECMAScript Versionen enthalten sein werden. Da TypeScript eine Übermenge von JavaScript ist, ist auch jede JavaScript-Anwendung, zumindest Anwendungen die mit ES5 geschrieben worden sind, eine valide TypeScript-Anwendung.

Wir werden uns nicht die komplette TypeScript-Funktionalität anschauen, sondern nur die Teile, die wir in den verschiedenen Rezepten brauchen werden. Der Grund dafür ist, dass wir uns auf Angular 2 konzentrieren und nicht zu viele Zeit mit TypeScript verbringen möchten. Um die komplette Funktionalität von TypeScript abzudecken, bräuchte man ein eigenes Buch. Der große Vorteile von TypeScript gegenüber JavaScript ist das Typ-System, welches uns TypeScript zur Verfügung stellt. Dieses ermöglicht uns, Typinformationen für Variablen, Funktionen, Objekte und mehr zu hinterlegen. In kleineren Anwendungen ist dieser Vorteil vielleicht nicht so relevant, da wir dort relativ schnell sehen können, welche Datentypen wo verwendet werden. Wer aber größere JavaScript-Anwendungen geschrieben hat, weiß, wie schwer es sein kann, den Überblick zu bewahren und herauszufinden welche Eigenschaften ein bestimmtes Objekt hat. Mit Hilfe von Typinformationen können wir solche Probleme vermeiden. Da Typen ein so wichtiger Aspekt von TypeScript sind, werden wir uns zuerst damit befassen.

Basistypen

TypeScript bringt von sich aus eine Anzahl von Basistypen wie z. B. “string”, “boolean” und “number” mit, aber es erlaubt es uns auch eigene Typen zu definieren. Es ist zwar nicht erforderlich, dass wir mit dem Typ-System arbeiten, es kann aber manchmal ganz nützlich sein. Darum werden wir in den verschiedenen Rezepten immer wieder auf Typen stoßen.

Insgesamt hat TypeScript zehn Typen, die immer vorhanden sind:

  • boolean
  • number
  • string
  • array
  • tuple
  • enum
  • any
  • void
  • undefined und null
  • never

Typdefinitionen kommen immer nach einem Doppelpunkt (:). Wenn wir z. B. nach einem Variablennamen, Funktionsnamen oder Funktionsparameter einen Doppelpunkt sehen, dann handelt es sich um eine Typdefinition. Der Wert nach dem Doppelpunkt gibt den Typ an. Beim Kompilieren werden die Typinformationen benutzt, um sicherzustellen, dass wir der Variable nur Werte des richtigen Typs zuweisen. Der kompilierte JavaScript-Code enthält diese Informationen nicht mehr. Auf dem TypeScript Playground können wir TypeScript-Code schreiben und sehen wie der dazugehörige JavaScript-Code aussieht.

Boolean

Dieser Typ ist für boolesche Werte gedacht und beinhaltet die Werte true und false. Der Name des Typs ist “boolean”.

1 var isTrue: boolean = false;

Number

Wird für Ganz- und Gleitkommazahlen verwendet. Der Typ heißt in diesem Fall “number”.

1 var aNumber: number = 2;

String

Texte haben den Typ “string”. Es ist dabei egal, ob wir einfache Anführungszeichen ('), doppelte Anführungszeichen (") oder Backticks (`) nutzen, der Typ bleibt gleich. Backticks werden für ES6/ES2015 Template Literals verwendet, welche auch von TypeScript unterstützt werden. Die unten gezeigten Beispiele sind alle valide Werte vom Typ “string”.

1 var aString: string = 'A string';
2 aString = "another string";
3 aString = `yet another string`;

Array

Der Typ “array” wird für Listen verwendet. Damit die Typdefinition einen Sinn ergibt, müssen wir auch den Typ der Elemente der Liste angeben. Unten werden die zwei Möglichkeiten gezeigt, die es gibt, um den Typ einer Liste zu definieren.

1 var list1: number[] = [1, 2, 3];
2 var list2: Array<number> = [1, 2, 3];

Erklärung:

  • Zeile 1: Eine Liste von Zahlen definieren. Als Erstes haben wir gesagt, dass der Typ der Elemente “number” ist. Mit den eckigen Klammern haben wir TypeScript mitgeteilt, dass es sich um ein Array handelt
  • Zeile 2: Auch eine Liste von Zahlen, diesmal mit generischer Typdefinition. Die Kleiner- (<) und Größerzeichen (>) geben an, dass es sich um einen generischen Typ handelt. Das TypeScript-Handbuch stellt Informationen über generische Typen bereit

Tuple

Der Typ “tuple” wird auch für Listen verwendet. Mit Hilfe dieses Typs, können wir Listen definieren, bei welchen die Elemente an verschiedenen Positionen unterschiedliche Typen besitzen. Hier ein kleines Beispiel:

1 var x: [string, number] = ['bla', 10];

Erklärung:

Hier definieren wir eine Variable namens “x” als Liste, wobei das erste Element der Liste ein String sein muss und das Zweite eine Zahl. Wir haben nur die erste und die zweite Positione der Liste mit einem Typ versehen. Die weiteren Positionen der Liste können entweder Werte vom Typ “string” oder vom Typ “number” sein. Dieses “entweder … oder …” für Typen nennt man “Union Type” (Vereinigung von Typen). Im TypeScript-Handbuch befinden sich weitere Informationen über Union Types. Damit wir ein Tupel definieren können, müssen wir den Typ für mindestens das erste Listenelement definieren.

Enum

Dieser Typ wird für Aufzählungen benutzt. Damit können wir entwicklerfreundlichen Namen für numerische Werten angeben. Der Namen des Typs ist “enum”.

1 // Enumdefinition
2 enum Status {DONE, IN_PROGRESS, NEW};
3 // status Variable hat den Status "NEW"
4 var status: Status = Status.NEW;

Erklärung:

Beim Kompilieren werden die Werte DONE, IN_PROGRESS und NEW in Zahlen von 0 bis 2 umgewandelt. Enums bieten uns noch mehr Möglichkeiten an, z. B. können wir selbst definieren, ob die Zahlen von 0 oder von 1 anfangen. Weitere Informationen über Enums gibt es im TypeScript-Handbuch.

Any

Hier reden wir nicht über einen echten Typ, sondern über eine Möglichkeit, TypeScript zu sagen, dass wir den Typ nicht oder noch nicht kennen und dass TypeScript sich in diesem Fall beim Kompilieren nicht beschwerden soll, wenn z. B. die Variable nicht den richtigen Typ hat. Dank des any-Typs können wir existierenden JavaScript-Code als TypeScript-Code behandeln, ohne dass wir für jede Variable und Funktion explizit einen Typ definieren müssen. Den any-Typ können wir auch benutzen, wenn wir, wie hier, ein Array mit Elementen unterschiedlicher Typen haben.

1 var list: Array<any> = [1, true, 'false'];

Void

Void ist sozusagen der leere Typ oder einfach die Abwesenheit eines Typs. Dieser Typ wird oft bei Funktionen verwendet, die keinen Rückgabewert haben. Werte vom Typ “void” sind null und undefined.

1 function test(): void {
2   console.log('test');
3 }

Undefined und Null

In TypeScript haben die Werte null und undefined einen eigenen Typ der “null” bzw. “undefined” heißt.

1 var foo: undefined = undefined;
2 var bar: null = null;

Im Regelfall sind die Typen “undefined” und “null” Subtypen von jedem anderen Typ. Das bedeutet, dass wir z. B. eine Variable vom Typ “number” haben können die als Wert eine Zahl, undefined oder null haben kann.

1 var num: number = 1;
2 num = undefined;
3 num = null;

Wenn wir aber strictNullChecks nutzen, sind die Werte null und undefined nicht in jedem Typ enthalten. Die Zeilen 2 und 3 im Beispiel oben währen dann keine gültige Zuweisung im Sinne von TypeScript. Mit stringNullChecks, können wir null an Variablen vom Typ “void” und “null” zuweisen. Entsprechend können wir den Wert undefined nur an Variablen vom Typ “void” und “undefined” zuweisen. Diese zwei Typen können nur dann sinnvoll eingesetzt werden, wenn wir strictNullChecks nutzen.

Never

Der “never”-Typ ist ein Typ ohne Wert und repräsentiert etwas was nie passieren kann. Z. B. eine Funktion die immer eine Exception schmeisst, kann als Rückgabetyp “never” haben, da in so einem Fall die Funktion nie ein Rückgabewert haben kann.

1 function throwError(): never {
2   throw Error('Some error');
3 }

Wir werden uns in diesem Buch mit dem “never”-Typ nicht weiter beschäftigen. Dieser wird nur in speziellen Situationen benutzt wie z. B. in Type Guards.

Interfaces

Nachdem wir uns die Basistypen von TypeScript angeschaut haben, werden wir jetzt sehen, wie wir den Typ von Objekten mit Hilfe von Interfaces definieren können. Genauer gesagt definieren wir mit Interfaces die Struktur eines Objekts. Wir geben Typen für dessen Eigenschaften und Methoden an.

Wir haben zwei Möglichkeiten, ein Interface zu definieren. Einmal als anonymes Interface (inline annotation) z. B. bei einer Variablendefinition oder als benanntes Interface mit dem Keyword interface. In beiden Fällen wird der kompilierte JavaScript-Code den Code für das Interface nicht beinhalten. Zuerst schauen wir uns anonyme Interfaces an.

Typdefinition für ein Objekt mit einem anonymen Interface
1 var user: {name: string; age: number};
2 user = {
3   name: 'Max',
4   age: 23
5 };

Erklärung:

Hier wird erwartet, dass die user-Variable ein Objekt mit mindestens den Eigenschaften “name” und “age” ist. Falls diese Eigenschaften nicht vorhanden sind oder nicht den richtigen Typ haben, wird der Compiler uns warnen. Das user-Objekt darf auch mehr als nur diese beiden Eigenschaften haben.

Benannte Interfaces haben die gleiche Schreibweise mit gewissen Unterschieden:

  • Sie haben einen Namen
  • Sie brauchen das Keyword interface
  • Sie sind nicht Teil der Variablendeklaration, sondern eine Deklaration für sich
Typdefinition für ein Objekt mit einem benannten Interface
1 interface User {
2   name: string;
3   age: number;
4 }
5 var user: User;
6 user = {
7   name: 'Max',
8   age: 23
9 };

Erklärung:

Erst wird das Interface definiert (Zeile 1-4) und dann in Zeile 5 benutzt. Ansonsten gilt für den Typ das Gleiche wie schon oben erklärt.

Wir haben hier die einfachste Form eines Interfaces gezeigt. TypeScript bietet uns noch weitere Möglichkeiten an. Z. B. gibt es Interfaces mit optionalen Eigenschaften, Interfaces für Funktionen und mehr. Wer mehr darüber erfahren möchte, kann im TypeScript-Handbuch nachschauen. Im nächsten Abschnitt werden wir uns mit Klassen beschäftigen. Wir werden sehen, wie wir Interfaces mit Klassen kombinieren können.

Klassen

Klassen in TypeScript sind ähnlich zu ES6/ES2015-Klassen. Beide bieten uns eine einfache Möglichkeit, in JavaScript bzw. TypeScript objektorientiert zu programmieren. Auch wenn wir das Keyword class nutzen, arbeiten wir hier nicht mit echten Klassen, wie wir diese aus anderen Programmiersprachen wie z. B. Java kennen. Als Grundlage für Klassen in JavaScript bzw. TypeScript dient immer noch der Prototyp. Zuerst werden wir uns die Schreibweise für ES6/ES2015-Klassen anschauen. Danach zeigen wir die dazugehörige ES5-Schreibweise. Und als Letztes werden wir sehen, wie man Klassen in TypeScript definiert.

ES6/ES2015-Klassen und die dazugehörige ES5-Schreibweise

ES6/ES2015-Klasse
 1 // Klassendefinition
 2 class User {
 3   constructor(name) {
 4     this.name = name;
 5   }
 6   print() {
 7     console.log(this.name);
 8   }
 9 }
10 
11 // Nutzung
12 var user = new User('Max');

Erklärung:

  • Zeile 2: Nach dem class-Keyword steht der Name der Klasse, in unserem Fall “User”
  • Zeilen 3-5: Die Klasse hat eine (optionale) Konstruktorfunktion mit dem Parameter “name”. Diese wird aufgerufen, wenn wir, wie in Zeile 12, new benutzen
  • Zeile 6: Methode namens “print”
Die User-Klasse in ES5-Schreibweise
 1 // Konstruktorfunktion
 2 function User(name) {
 3   this.name = name;
 4 }
 5 // Prototypmethoden
 6 User.prototype.print = function() {
 7   console.log(this.name);
 8 };
 9 
10 var user = new User('Max');

Erklärung:

Zeile 2 definiert eine Konstruktorfunktion mit dem Namen “User”. In ES6/ES2015/TypeScript wird dieser Name als Klassenname benutzt. Der Rumpf dieser Funktion und ihre Parameter definieren die Konstruktorfunktion der Klasse. Methoden einer Klasse entsprechen in ES5 Methoden, die zu der prototype-Eigenschaft der Konstruktorfunktion gehören.

TypeScript-Klassen

Neben Interfaces bieten TypeScript-Klassen eine weitere Möglichkeit, Typen für Objekte zu definieren. Interfaces definieren die Typen der Eigenschaften und Methoden eines Objekts, wohingegen Klassen nicht nur Typen, sondern auch das Verhalten und Werte für die Eigenschaften definieren. Der Klassenname ist auch gleichzeitig der Typname der Instanzen einer Klasse. Wir können also den Namen einer Klasse bei einer Typdefinition genauso nutzen, wie wir es für Interfaces getan haben.

TypeScript-Klasse
 1 class User {
 2   name: string;
 3   constructor(name: string) {
 4     this.name = name;
 5   }
 6   print(): void {
 7     console.log(this.name);
 8   }
 9 }
10 
11 var user: User;
12 user = new User('Max');

Erklärung:

In Zeile 2 sagen wir TypeScript, dass unsere Instanzen der Klasse “User” eine Eigenschaft namens “name” mit Typ “string” haben. Das ist einer der Unterschiede zwischen TypeScript und ES6/ES2015 Klassen. Da wir in TypeScript mit Typen arbeiten, können wir natürlich auch Typinformationen in unseren Klassen hinterlegen. Wie immer ist die Typangabe optional, aber wir müssen den Namen der Eigenschaft angeben. Andernfalls warnt der Compiler. Wir können also Zeile 2 auch so schreiben: name; ohne die Typangabe. In der vorletzte Zeile definieren wir eine Variable namens “user” vom Typ “User”. Anschließend wird in der letzten Zeile der user-Variablen eine Instanz der User-Klasse zugewiesen.

Eine weitere Schreibweise von TypeScript-Klassen
 1 class User {
 2   name: string = '';
 3   constructor(name: string) {
 4     this.name = name;
 5   }
 6   print(): void {
 7     console.log(this.name);
 8   }
 9 }
10 
11 var user: User;
12 user = new User('Max');

Erklärung:

Diesesmal haben wir der name-Eigenschaft einen Wert zugewiesen (Zeile 2). Diese Schreibweise ist vor allem nützlich, wenn wir der Eigenschaft einen Initialwert geben möchten oder wenn wir mit statischen Daten arbeiten. Genau wie oben ist auch hier die Typdefinition optional. Die gezeigte Schreibweise ist in ES6/ES2015-Klassen nicht erlaubt. Dort dürfen Eigenschaften nur dem this-Wert zugewiesen und nicht als Teil der Klassendefinition benutzt werden. Es ist aber möglich, dass spätere Versionen von ECMAScript-Klassen dies erlauben, natürlich ohne die Typinformation.

Klassen mit Interfaces

Ein weiterer Vorteil von TypeScript-Klassen gegenüber ES6/ES2015-Klassen ist, dass TypeScript-Klassen ein Interface implementieren können. Dazu nutzt man das Keyword implements bei der Klassendefinition.

TypeScript-Klasse mit Interface
 1 interface IUser {
 2   name: string;
 3   print(): void;
 4 }
 5 
 6 class User implements IUser {
 7   name: string;
 8   constructor(name: string) {
 9     this.name = name;
10   }
11   print(): void {
12     console.log(this.name);
13   }
14 }

Erklärung:

  • Zeilen 1-4: Interfacedefinition (Siehe auch Interfaces)
    • Zeile 3: Typdefinition für eine Methode. Der Name der Methode ist “print”, sie hat keine Parameter und der Rückgabetyp ist “void”
  • Zeilen 6-14: Klassendefinition
    • Zeile 6: Nutzung des Keywords implements heißt für uns, dass User-Instanzen vom Typ “IUser” sein müssen

Im Allgemeinen können TypeScript-Klassen noch mehr als hier beschrieben, aber das hier beschriebene reicht uns, um die Angular 2 Rezepte zu verstehen. Wer mehr über TypeScript-Klassen erfahren möchte, kann dies hier nachlesen.

Beispielanwendung

Um besser zu verstehen, wie wir mit TypeScript arbeiten können, gibt es hier noch eine kleine Beispielanwendung. Weil Todo-Listen als Beispielanwendung mittlerweile zum Standard geworden sind, haben wir uns entschieden, dass unsere Anwendung ebenfalls eine Todo-Liste sein soll. Die Anwendung kann vordefinierte Todos anzeigen und neue Todos in einer existierenden Liste von Todos hinzufügen. Obwohl die Todo-Anwendung klein ist, ist sie trotzdem in mehrere Dateien aufgespalten. Wir wollen damit zeigen, wie man mit Hilfe von ECMAScript-Modulen (ESM) eine Anwendung modular aufbauen kann. Wenn wir mit ESM arbeiten, ist jede Datei auch ein Modul. Der komplette Code für die Anwendung befindet sich in Github unter 01-TypeScript/01-Simple_Todo_App.

Code für die Anwendung

Der Einstiegspunkt für die Anwendung ist die Datei index.html. In dieser laden wir die Anwendung und zeigen sie im Browser an. Im Verzeichnis “app” befinden sich unsere TypeScript-Dateien. Im app-Verzeichnis gibt es drei Dateien namens “main.ts”, “todo_item.ts” und “todo_list.ts”.

index.html
 1 <!DOCTYPE html>
 2 <html>
 3 <head>
 4   <meta charset="utf-8">
 5   <title>TypeScript - Todo App</title>
 6   <script src="https://code.angularjs.org/tools/system.js"></script>
 7   <script src="https://code.angularjs.org/tools/typescript.js"></script>
 8   <script>
 9     System.config({
10       transpiler: 'typescript',
11       packages: {'app': {defaultExtension: 'ts'}}
12     });
13     System.import('./app/main');
14   </script>
15 </head>
16 <body>
17   <form>
18   <input id="todoTitle"/>
19   <button type="submit" id="addTodo">Add</button>
20 </form>
21   <ul id="todosList"></ul>
22 </body>
23 </html>

Erklärung:

  • Zeile 6: Laden von SystemJS
  • Zeile 7: Laden des TypeScript-Compilers
  • Zeilen 8-14: Konfiguration von SystemJS und Laden der Anwendung
    • Zeile 10: Hier teilen wir SystemJS mit, dass unsere TypeScript-Dateien on-the-fly kompiliert werden sollen
    • Zeile 11: Hier sagen wir SystemJS, dass alle Dateien im Verzeichnis “app” eine “.ts” Endung haben. Somit brauchen wir beim Importieren eines Moduls die Endung nicht anzugeben
    • Zeile 13: Laden der main.ts-Datei, das Hauptmodul unserer Anwendung
app/main.ts
 1 import TodoList from './todo_list';
 2 import TodoItem from './todo_item';
 3 
 4 const todos: Array<TodoItem> = [new TodoItem('Todo 1'), new TodoItem('Todo 2')];
 5 
 6 const inputElement: HTMLInputElement =
 7   document.getElementsByTagName('input').item(0);
 8 const button: HTMLElement = document.getElementById('addTodo');
 9 const todosList: HTMLElement = document.getElementById('todosList');
10 
11 const todoList = new TodoList(todos);
12 
13 todoList.render(todosList);
14 
15 button.addEventListener('click', function(event) {
16   event.preventDefault();
17   const todoTitle: string = inputElement.value;
18   todoList.add(new TodoItem(todoTitle));
19   todoList.clear(todosList);
20   todoList.render(todosList);
21 });

Erklärung:

Dies ist das Hauptmodul unserer Anwendung. Es instantiiert unsere vordefinierten Todos und die Liste von Todos. Es hat Zugriff auf DOM-Elemente und ruft Methoden auf, um die existierende Todos anzuzeigen und neue Todos hinzuzufügen.

  • Zeilen 1-2: Import der Module “TodoList” und “TodoItem” mittels ESM import-Anweisung. Wir nutzen hier den Namen der Datei ohne Endung, da wir SystemJS schon gesagt haben, dass Dateien im app-Verzeichnis immer die Endung “.ts” haben (Siehe index.html Zeile 14)
  • Zeile 4: Todos für unsere Liste. Die Liste beinhaltet Elemente vom Typ “TodoItem”
  • Zeilen 6-9: DOM-Elemente an Konstanten zuweisen. Wir nutzen dafür das ES6/ES2015 Keyword const. Die Typen “HTMLInputElement” und “HTMLElement” sind in TypeScript vordefiniert
app/todo_item.ts
 1 class TodoItem {
 2   title: string;
 3   checked: boolean;
 4   constructor(title: string) {
 5     this.title = title;
 6     this.checked = false;
 7   }
 8   render(listItem: HTMLElement): HTMLElement {
 9     const checkbox: HTMLInputElement = document.createElement('input');
10     const label: HTMLLabelElement = document.createElement('label');
11 
12     checkbox.type = 'checkbox';
13     checkbox.checked = this.checked;
14 
15     label.textContent = this.title;
16 
17     listItem.appendChild(checkbox);
18     listItem.appendChild(label);
19 
20     return listItem;
21   }
22 }
23 
24 export default TodoItem;

Erklärung:

Modul und Klassendefinition für ein Todo-Element. Unsere Klasse erzeugt Instanzen vom Typ “TodoItem”. In der letzten Zeile nutzen wir eine ESM export-Anweisung, um die Klasse zu exportieren. Somit können wir diese in anderen Modulen importieren und nutzen.

app/todo_list.ts
 1 import TodoItem from './todo_item';
 2 
 3 class TodoList {
 4   todos: Array<TodoItem>;
 5   constructor(todos: Array<TodoItem>) {
 6     this.todos = todos;
 7   }
 8   render(listElement: HTMLElement) {
 9     this.todos.forEach((todo: TodoItem) => {
10       const listItem: HTMLLIElement = document.createElement('li');
11       listElement.appendChild(todo.render(listItem));
12     });
13   }
14   add(todo: TodoItem) {
15     this.todos.push(todo);
16   }
17   clear(listElement: HTMLElement) {
18     listElement.innerHTML = '';
19   }
20 }
21 
22 export default TodoList;

Erklärung:

Modul/Klasse für die Todo-Liste. Unsere Klasse hat drei Methoden, “render”, “add” und “clear” und eine Eigenschaft vom Typ “Array<TodoItem>” namens “todos”.

  • Zeile 1: Hier wird das TodoItem-Modul importiert, um Zugriff auf die TodoItem-Klasse zu bekommen. Da wir die Klasse nur als Typdefinition nutzen, wird dieser Import nicht im kompilierten Code vorkommen
  • Zeile 9: Statt einer normalen Funktion (Keyword function) nutzen wir hier eine ES6/ES2015 Arrow-Funktion. Arrow-Funktionen sind kürzer zu schreiben und haben die Eigenschaft, dass sie den this-Wert ihrer Umgebung nutzen und keinen eigenen this-Wert definieren

Die Anwendung im Browser laden

Da SystemJS Ajax nutzt, um die einzelnen Module asynchron zu laden, brauchen wir einen Webserver, um unsere Todo-Anwendung im Browser zu laden. Das Angular-Team empfiehlt den live-server, der die Seite bei Änderungen automatisch neu laden kann. Wer kein live-reload mag, kann auch den http-server nutzen. Beide Webserver sind über npm installierbar. Natürlich kann man auch andere Webserver nutzen, wie z. B. Apache, nginx oder Webserver, die in einer IDE integriert sind. Nachdem wir einen Webserver gestartet haben, können wir im Browser zu der richtigen URL navigieren und uns die Anwendung ansehen.

TypeScript-Dateien vorkompilieren

Wie schon erwähnt, ist das on-the-fly-Kompilieren von TypeScript-Dateien auf Dauer keine Lösung. In diesem Abschnitt werden wir sehen, wie wir die TypeScript-Dateien vor dem Laden kompilieren können. Als Erstes benötigen wir den TypeScript-Compiler. Es gibt verschiedene Möglichkeiten, diesen herunterzuladen und zu installieren. Wir werden hier mit Node.js und npm arbeiten, da diese Tools weit verbreitet und einfach zu nutzen sind. Wir können Node.js installieren, indem wir es von der offiziellen Webseite herunterladen. Bei der Installation von Node.js wird npm mit installiert. Anschließend können wir den TypeScript-Compiler mit

1 npm install -g typescript

installieren.

Wir nehmen jetzt die Todo-Anwendung aus dem vorherigen Abschnitt und passen diese so an, dass die TypeScript-Dateien nicht mehr im Browser kompiliert werden. Dazu müssen wir zwei Sachen machen: Erstens muss die index.html-Datei angepasst werden und zweitens müssen wir die TypeScript-Dateien kompilieren. Der Code für die Anwendung mit angepasster index.html-Datei befindet sich in 01-TypeScript/02-Precompile.

Anpassungen an der index.html-Datei
 1 <!DOCTYPE html>
 2 <html>
 3 <head>
 4   <meta charset="utf-8">
 5   <title>TypeScript - Todo App</title>
 6   <script src="https://code.angularjs.org/tools/system.js"></script>
 7   <script>
 8     System.config({
 9       packages: {'app': {defaultExtension: 'js'}}
10     });
11     System.import('./app/main');
12   </script>
13 </head>
14 <body>
15   <form>
16     <input id="todoTitle"/>
17     <button type="submit" id="addTodo">Add</button>
18   </form>
19   <ul id="todosList"></ul>
20 </body>
21 </html>

Erklärung:

Der TypeScript-Compiler wird jetzt nicht mehr in der index.html-Datei geladen. In der SystemJS-Konfiguration haben wir die transpiler-Eigenschaft entfernt. Einen weiteren Unterschied sehen wir in Zeile 9, wo wir jetzt “.js” als Endung nutzen und nicht mehr “.ts”. Der Grund dafür ist, dass wir nun die kompilierten JavaScript-Dateien laden möchten. Jetzt müssen wir nur noch die TypeScript-Dateien kompilieren. Weitere Anpassungen sind nicht nötig.

Dateien kompilieren
1 tsc --target ES5 --module commonjs app/main.ts

Erklärung:

tsc ist der TypeScript-Compiler. Die Option “module” gibt an, dass die ESM, die wir nutzen, in CommonJS-Module umgewandelt werden sollen. Die Option “target” gibt an, welcher ECMAScript-Version unser JavaScript entsprechen soll. Hier nutzen wir ECMAScript 5. Als Letztes geben wir die main.ts-Datei an. Die weiteren Module, die die main.ts-Datei importiert, werden automatisch mit kompiliert. Wir müssen also nicht jede Datei einzeln kompilieren. Der TypeScript-Compiler bietet noch mehr Optionen an, die wir nutzen können. Zwei davon werden wir gleich noch sehen. Weitere Optionen werden im TypeScript-Wiki erläutert.

Dateien automatisch kompilieren mit “watch”

Bei jeder Änderung die Dateien manuell zu kompilieren, kann auf Dauer nerven. Dafür bietet uns der Compiler eine einfache Lösung. Es gibt eine Option namens “watch”. Mit dieser Option werden die Dateien automatisch bei jeder Änderung kompiliert.

Kommando mit watch
1 tsc --target ES5 --module commonjs --watch app/main.ts

Erklärung:

Mit der watch-Option werden unsere Dateien bei jeder Änderung automatisch neukompiliert. Das gilt nicht nur für die angegebene app/main.ts-Datei, sondern auch für alle Dateien, die importiert werden.

Sourcemaps generieren

Nach dem Kompilieren stimmen meistens die Zeilennummern in der JavaScript- und der TypeScript-Dateien nicht mehr überein. Das kann das Debugging erschweren, wenn z. B. der Browser einen Fehler in der JavaScript-Datei findet und wir diesen in der TypeScript-Datei finden und korrigieren möchten. Für genau solche Fälle gibt es Sourcemaps, die uns die richtige Zeile in der TypeScript-Datei anzeigen. Um Sourcemaps zu erzeugen, nutzen wir eine weitere Option des Compilers.

Sourcemaps generieren
1 tsc --target ES5 --module commonjs --sourceMap app/main.ts

Erklärung:

Die Sourcemaps werden im gleichen Verzeichnis wie die JavaScript-Dateien abgelegt und automatisch vom Browser geladen. Wir können auch “watch” mit “sourceMap” kombinieren, wenn wir das möchten.

Konfigurationsdatei für den Compiler nutzen

Auf Dauer kann es nerven, die ewig lange Zeile einzutippen, um unser Projekt zu kompilieren. Eine Alternative hierfür bietet die tsconfig.json-Datei. Darin können wir alle nötige Optionen angeben und dann den Compiler aufrufen, ohne selbst die Optionen angeben zu müssen.

tsconfig.json
1 {
2   "compilerOptions": {
3     "module": "commonjs",
4     "sourceMap": true,
5     "target": "ES5"
6   }
7 }

Das Verzeichnis, in dem sich die tsconfig.json-Datei befindet, ist das Hauptverzeichnis unseres TypeScript-Projektes. Nachdem wir die config-Datei erstellt haben, haben wir zwei Möglichkeiten, unsere Anwendung zu kompilieren.

Wir können

1 tsc

im Haupt- oder einem Unterverzeichnis unserer Anwendung aufrufen oder wir können

1 tsc -p Hauptverzeichnis

aufrufen, wobei Hauptverzeichnis der Pfad zu dem Verzeichnis ist, in dem die tsconfig.json-Datei liegt. Da unsere config-Datei die files-Eigenschaft nicht setzt, werden alle *.ts-Dateien kompiliert, die sich im Haupt- und in den Unterverzeichnissen befinden. Das TypeScript-Handbuch bietet weitere Informationen über die tsconfig.json-Datei an und die Eigenschaften, die diese enthalten kann.

Basisrezepte

In diesem Kapitel befinden sich Basisrezepte, die in späteren Rezepten als Zutaten benötigt werden. Der Code, der in den Lösungen gezeigt wird, kann kopiert und angepasst werden, um die Lösungen für weitere Rezepte zu implementieren. Es wird empfohlen, dieses Kapitel als Erstes zu lesen, um einen Überblick zu bekommen und sich erst dann mit den weiteren Rezepten zu beschäftigen. Das Ziel des Kapitels ist, einen schnellen Einstieg in die Grundlagen und die Hauptbauteile von Angular 2 Anwendungen zu ermöglichen.

Entwicklungsprozess für Angular 2 Projekte

Problem

Ich möchte einen möglichst einfachen Weg haben, ein Angular 2 Projekt zu initialisieren, zu bauen und das Resultat im Browser anzuschauen.

Zutaten

  • Node.js
  • npm
  • angular-cli

Lösung

Der derzeit einfachste Weg, ein Angular 2 Projekt zu starten, zu bauen und sich das Resultat im Browser anzuschauen, ist ein Tool namens “angular-cli”. Das Tool befindet sich noch im beta-Stadium. Nichtsdestotrotz werden wir es nutzen, da die Alternative (alles selbst einzurichten) sehr aufwändig ist.

Als Erstes müssen wir Node.js und npm installieren, damit wir im zweiten Schritt angular-cli installieren können. Am einfachsten können wir Node.js installieren, indem wir es von der offiziellen Webseite herunterladen. Bei der Installation von Node.js, wird npm mit installiert. Jetzt können wir angular-cli installieren mit:

1 npm install -g angular-cli@1.0.0-beta.19

Wir installieren angular-cli global und können es daher in jedem Angular 2 Projekt nutzen.

Jetzt können wir ein Projekt initialisieren. Dafür gibt es die Kommandos “new” und “init”.

1 ng new projektName --skip-git

Dieses Kommando wird ein Verzeichnis mit dem Namen “projektName” erzeugen. Darin wird das Tool die nötigen Verzeichnisse/Dateien anlegen und alle Abhängigkeiten mittels npm installieren. Falls --skip-git nicht angegeben wird, wird angular-cli auch ein git-Repository anlegen, vorausgesetzt, dass wir nicht schon in einem git-Repository sind.

Das init-Kommando macht das gleiche wie das new-Kommando aber für ein existierendes Verzeichnis.

1 ng init --name projektName

Falls --name projektName nicht angegeben wird, wird der Name des Verzeichnisses als Projektname benutzt.

Anwendung starten

Nachdem alle Abhängigkeiten installiert worden sind, können wir die Anwendung starten. Angular-cli hat einen eingebauten HTTP-Server, den wir dafür nutzen können. Um den Server zu starten, müssen wir im Projekt-Verzeichnis (das mit der package.json-Datei) folgendes Kommando aufrufen:

1 ng serve

In der Konsole steht dann, zu welcher URL wir navigieren müssen, um die Demo-Anwendung von angular-cli zu sehen (Zeile: “Serving on http://…”). Das Nette an diesem Webserver ist der Live-Reload-Support. Das heißt, wenn wir Änderungen im Code machen, werden diese sofort im Browser sichtbar, ohne dass wir das Projekt selbst erneut kompilieren müssen.

Diskussion

Alle Rezepte in diesem Buch wurden mit angular-cli initialisiert. Es ist also nicht nötig ng init oder ng new aufzurufen. Es reicht, wenn npm install aufgerufen wird, um die Abhängigkeiten zu installieren. Danach können wir, wie oben gezeigt, mit ng serve die Anwendung starten.

Da das Tool eine eigene Meinung hat, wie die Verzeichnisstruktur, eine Komponente usw. auszusehen hat, wurden aus den Code-Beispielen ein paar Verzeichnisse/Dateien, die nicht für das jeweilige Beispiel relevant sind, gelöscht bzw. angepasst. Wir wollen nicht, dass überflüssige Verzeichnisse, Dateien und Code-Zeilen uns vom eigentlichen Thema eines Rezeptes ablenken. Wir schauen uns also nur die relevanten Dateien für ein Rezept an und ignorieren den Rest. Auch gewisse Abhängigkeiten wurden aus der package.json-Datei entfernt, um die Installationszeit zu verkürzen. Es ist also möglich, dass nicht alle angular-cli Kommandos mit jedem Rezept funktionieren. Für die meisten Rezepte ist das src-Verzeichnis am Wichtigsten. Darin befindet sich der Code für eine Angular 2 Anwendung. Mehr Informationen über die Verzeichnisstruktur gibt es in Appendix-B: angular-cli.

Weitere Ressourcen

Angular 2 Anwendung

Problem

Ich möchte von Null auf eine Angular 2 Anwendung implementieren.

Zutaten

  • angular-cli
  • app.module.ts-Datei, die das Hauptmodul der Anwendung definiert
  • NgModule-Decorator für die Moduldefinition (@NgModule)
  • app.component.ts-Datei, die die Hauptkomponente der Anwendung definiert
  • Component-Decorator für die Komponentendefinition (@Component)
  • main.ts-Datei. Diese Datei initialisiert (bootstrap) die Angular 2 Anwendung
  • index.html-Datei, um die nötigen Bibliotheken zu laden und die Anwendung zu starten

Lösung

Als Erstes werden wir uns die app.component.ts-Datei anschauen. Diese Datei befindet sich im Unterverzeichnis “src/app”.

app.component.ts
1 import { Component } from '@angular/core';
2 
3 @Component({
4   selector: 'app-root',
5   template: '<div>Hello World!</div>'
6 })
7 export class AppComponent {}

Erklärung:

Diese Datei definiert die Haupt- und in diesem Fall einzige Komponente unserer Anwendung. Sie ist ein ECMAScript-Modul (ESM). Jede Datei, die das Keyword import bzw. das Keyword export beinhaltet ist ein ESM.

  • Zeile 1: Hier importieren wir die nötigen Abhängigkeiten aus dem @angular/core-Paket. Dafür nutzen wir eine ESM import-Anweisung
  • Zeilen 3-6: Hier definieren wir eine Komponente mittels TypeScript-Decorator
    • Zeile 4: Die selector-Eigenschaft definiert das Tag in dem die Komponente gerendert werden soll. Hier wird die Komponente im Tag <app-root> gerendert
    • Zeile 5: Der Wert der template-Eigenschaft ist ein Angular-Template. Es wird später von Angular kompiliert und zwischen <app-root> und </app-root> hinzugefügt. Das kompilierte Angular-Template wird als “View” der Komponente bezeichnet
  • Zeile 7: Definiert eine TypeScript-Klasse, die die Logik für die Komponente beinhaltet. In diesem Fall ist die Klasse leer, da wir keine Logik benötigen
    • Gleichzeitig wird die Klasse (und somit die Komponente) mit einer ESM export-Anweisung exportiert

Jetzt sehen wir uns unser Hauptmodul (app.module.ts) an. Diese Datei befindet sich auch im Unterverzeichnis “src/app”.

app.module.ts
 1 import { NgModule } from '@angular/core';
 2 import { BrowserModule } from '@angular/platform-browser';
 3 
 4 import { AppComponent } from './app.component';
 5 
 6 @NgModule({
 7   imports: [ BrowserModule ],
 8   declarations: [ AppComponent ],
 9   bootstrap: [ AppComponent ]
10 })
11 export class AppModule { }

Erklärung:

  • Zeile 1: Hier wird der NgModule-Decorator aus @angular/core importiert
  • Zeile 2: Importiert das BrowserModule aus @angular/platform-browser. Dieses bietet uns Funktionaliät an, die wir benötigen, wenn wir Angular im Browser nutzen wollen
  • Zeilen 6-10: Hier wird unser Modul mittels @NgModule definiert
    • Zeile 7: Damit wir funktionalität von anderen Angular-Module in unser Modul nutzen können, müssen diese im imports-Array stehen
    • Zeile 8: Hier wird unsere Komponente deklariert. Somit ist das Modul und Angular von ihre Existenz informiert
    • Zeile 9: Hier wird die Komponente definiert, die bei der Initialisierung der Anwendung als erstes initialisiert werden soll

Als nächstes werden wir uns die main.ts-Datei anschauen. Diese befindet sich im Unterverzeichnis “app”.

main.ts
1 import './polyfills.ts';
2 
3 import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
4 
5 import { AppModule } from './app';
6 
7 platformBrowserDynamic().bootstrapModule(AppModule);

Erklärung:

  • Zeile 1: Hier importieren wir eine Datei mit verschiedenen Polyfills die von Angular gebraucht werden. Falls nötig, können in dieser Datei weitere Polyfills für unsere Anwendung definieren
  • Zeile 5: Hier importieren wir unser Hauptmodul. Wenn wir aus einem Verzeichnis importieren (hier “./app”), wird automatisch in der index.ts-Datei nach dem passendem Namen (hier “AppModule”) gesucht. Diese Datei ist ein sogenanntes barrel und wird von angular-cli automatisch angelegt
  • Zeile 7: Das Hauptmodul wird der Initialisierungsfunktion (bootstrapModule) übergeben und die Anwendung wird initialisiert

Als Letztes schauen wir uns die index.html-Datei an. Diese befindet sich ebenfalls im Unterverzeichnis “app”.

index.html
1 ...
2 
3 <body>
4   <app-root>Loading...</app-root>
5 </body>

Erklärung:

Wir schauen uns nur ein Ausschnitt aus der Datei an und zwar den Teil, in dem unsere Anwendung gerendert wird (Zeile 4). Initial wird “Loading…” angezeigt bis Angular initialisiert wird.

Um den Beispiel-Code aus Github im Browser anzuzeigen, müssen wir mittels npm install die Abhängigkeiten installieren und dann mit ng serve den Server starten.

Diskussion

Unser Code-Beispiel nutzt Angular im Entwicklungsmodus. Darüber informiert uns auch Angular, wenn wir im Browser die Konsole offen haben. Im Rezept “Angular 2 in Produktion nutzen” wird beschrieben wie wir Angular 2 im Produktionsmodus nutzen können.

Angular-Plattformen

Angular definiert verschiedene sogenannte Plattformen, die eine Umgebung schaffen in der eine Angular-Anwendung laufen kann. Diese Plattformen definieren z. B. wann die Angular-Templates kompiliert werden oder stellen z. B. DOM-Anbindungen zur Verfügung. Je nach dem wo die Anwendung laufen soll, z. B. im Browser, Server usw. gibt es auch eine entsprechendes Plattform-ES-Modul/-npm-Paket. Wir haben “platform-browser-dynamic” verwendet weil unsere Anwendung im Browser mit Just-In-Time (JIT) Kompilierung arbeiten soll. In diesem Fall bedeutet JIT, dass die Templates im Browser kompiliert werden. Angular bietet auch die Option, Templates Ahead-Of-Time (AOT) beim bauen der Anwendung zu kompilieren. In so einem Fall würden wir in der main.ts “platform-browser” statt “platform-browser-dynamic” verwenden.

Bootstrap

Der/die eine oder andere Leser/Leserin mag sich jetzt fragen, warum wir nicht einfach eine TypeScript-Datei mit der Moduldefinition und der bootstrap-Funktion haben. Natürlich hätten wir das auch machen können, aber die Aufspaltung in zwei Dateien bringt ein Vorteil mit sich. Wir vermischen die Moduldefinition nicht mit der Initialisierung der Anwendung. Die Moduldefinition ist allgemein und könnte auf unterschiedlichen Plattformen verwendet werden. Die bootstrap-Funktion ist Plattformspezifisch. Jede Plattform hat eine eigene bootstrap-Funktion, die genau weiß wie die Anwendung initialisiert werden soll, damit diese auch auf der Plattform laufen kann. Das heißt, dass wir theoretisch mehrere Initialisierungsdateien haben könnten (z. B. eine für den Browser und eine für den Server), die das gleiche Hauptmodul verwenden. Die Aufspaltung erhöht also die Wiederverwendbarkeit des Moduls.

Code

Code auf Github

Live Demo auf angular2kochbuch.de

Weitere Ressourcen

  • Informationen über ECMAScript-Module
  • Weitere Eigenschaften des Component-Decorators sind auf der Angular 2 Webseite beschrieben: @Component
  • Weitere Eigenschaften des NgModule-Decorators sind auf der Angular 2 Webseite beschrieben: @NgModule
  • Weitere Informationen über Angular-Module gibt es hier
  • Mehr Informationen zu Decorators in TypeScript gibt es hier
  • Weitere Informationen zu Angular-Templates gibt es in Appendix A: Template-Syntax

Eine Komponente definieren

Problem

Ich möchte weitere Komponenten nutzen, um meine Anwendung modularer zu gestalten.

Zutaten

  • Angular 2 Anwendung
  • Datei für die neue Komponente (second.component.ts)
  • Anpassungen an der Hauptkomponente (app.component.ts), die wir im Rezept “Angular 2 Anwendung” definiert haben
  • Anpassungen am Hauptmodul (app.module.ts)

Lösung

second.component.ts
1 import { Component } from '@angular/core';
2 
3 @Component({
4   selector: 'app-second',
5   template: '<div>My Name is ...</div>'
6 })
7 export class SecondComponent {}

Erklärung:

Diese Datei ist auch ein ESM und beinhaltet die Komponentendefinition für eine Komponente namens “SecondComponent”. Genau wie unsere Hauptkomponente definiert diese die selector- und template-Eigenschaften.

app.component.ts
1 import { Component } from '@angular/core';
2 
3 import { SecondComponent } from './second.component';
4 
5 @Component({
6   selector: 'app-root',
7   template: '<div>Hello World!</div><app-second></app-second>'
8 })
9 export class AppComponent {}

Erklärung:

  • Zeile 3: Hier importieren wir unsere zweite Komponente
  • Zeile 7: Wir haben den Tag <app-second></app-second> zum Angular-Template hinzugefügt. Zu beachten ist, dass der Tag-Name gleich dem Selektor in Zeile 4 in der second.component.ts-Datei sein muss
app.module.ts
 1 import { NgModule } from '@angular/core';
 2 import { BrowserModule } from '@angular/platform-browser';
 3 
 4 import { AppComponent }  from './app.component';
 5 import { SecondComponent } from './second.component';
 6 
 7 @NgModule({
 8   imports: [ BrowserModule ],
 9   declarations: [ AppComponent, SecondComponent ],
10   bootstrap: [ AppComponent ]
11 })
12 export class AppModule { }

Erklärung:

  • Zeile 9: Hier deklarieren wir unsere neue Komponente, so dass wir diese in “AppComponent” nutzen können

Diskussion

Jede Komponente, Direktive und Pipe muss in genau einem Modul deklariert werden und gehört dann zu diesem Modul. Wenn wir diese nicht deklarieren, können wir sie in der Anwendung nicht nutzen. Falls wir eine Komponente, Direktive oder Pipe in mehr als ein Modul deklarieren, wird Angular eine Exception schmeißen.

Die Komponente “SecondComponent” ist jetzt eine Unterkomponente (auf Englisch child component) unserer Hauptkomponente. Indem wir Komponenten im Modul deklarieren und dann im Template nutzen, können wir beliebig große Komponentenbäume erzeugen. Tatsächlich ist eine Angular 2 Anwendung nur ein Baum von Komponenten, mit der Hauptkomponente an der Spitze und beliebig vielen Unterkomponenten.

Selektoren

Es wird empfohlen, ein Präfix für den Selektor der Komponente zu nutzen. Deshalb ist der Selektor der “SecondComponent” nicht nur “second”, sondern “app-second”. Der Präfix hat zwei Funktionen:

  • Einerseits zeigt dieser, dass eine Komponente von uns implementiert worden ist und nicht aus einer externen Quelle importiert wird
  • Anderseits können wir das Präfix nutzt, um anzugeben zu welchem Teil/Modul unserer Anwendung eine Komponente gehört

Bei größeren Anwendungen ist es nicht ungewöhnlich, die Anwendung auf mehrere Unterverzeichnisse zu verteilen, wobei jedes Verzeichnis ein Feature bezeichnet. Z. B. kann eine große Anwendung einen Nutzerbereich und einen Adminbereich haben. In so einem Fall können alle Komponenten des Nutzerbereichs das Präfix “user” erhalten und alle Komponenten des Adminbereichs das Präfix “admin”. Natürlich würden wir in so einem Fall auch für jedes Feature ein eigenes Angular-Modul definieren.

Code

Code auf Github

Live Demo auf angular2kochbuch.de

Weitere Ressourcen

  • Der Angular Styleguide gibt Hintergrundinformationen zu Namenskonventionen, zur Verzeichnisstruktur von Angular 2 Anwendungen und zu anderen Best Practices
  • Neue Komponenten können wir auch mit Hilfe des generate-Kommandos von angular-cli generieren. Mehr Informationen gibt es in Appendix-B: angular-cli

Einen Service definieren

Problem

Ich möchte einen Service definieren und nutzen, damit ich Teile meiner Logik und die Daten aus der Komponente entfernen kann.

Zutaten

  • Angular 2 Anwendung
  • Eine Datei für unseren Service (data.service.ts)
  • Injectable-Decorator (@Injectable)
  • Anpassungen an der app.component.ts- und der app.module.ts-Datei

Lösung

Was wir in der Angular-Welt einen Service nennen, ist im Grunde genommen nur eine TypeScript-Klasse. Services werden benutzt, um Daten und Logik außerhalb von Komponenten zu halten. Somit können wir die Methoden und die Daten eines Services in mehreren Komponenten wiederverwenden.

data.service.ts
 1 import { Injectable } from '@angular/core';
 2 
 3 const data = ['a', 'b', 'c'];
 4 
 5 @Injectable()
 6 export class DataService {
 7   data: Array<string>;
 8   constructor() {
 9     this.data = data;
10   }
11 
12   getData() {
13     return this.data;
14   }
15 }

Erklärung:

  • Zeile 5: Hier nutzen wir den Injectable-Decorator, um den Service als “Injectable” zu definieren
  • Zeilen 6-15: Diese Klasse repräsentiert unseren Service. Sie wird auch exportiert, so dass wir den Service in Komponenten und anderen Services nutzen können

Wir haben jetzt einen Service definiert und müssen jetzt diesen im Angular-Modul als Provider registrieren.

app.module.ts
 1 import { NgModule } from '@angular/core';
 2 import { BrowserModule } from '@angular/platform-browser';
 3 
 4 import { AppComponent } from './app.component';
 5 import { DataService } from './data.service';
 6 
 7 @NgModule({
 8   imports: [ BrowserModule ],
 9   declarations: [ AppComponent ],
10   bootstrap: [ AppComponent ],
11   providers: [ DataService ]
12 })
13 export class AppModule { }

Erklärung:

  • Zeile 11: Die providers-Eigenschaft teilt Angular mit, welche Service der Anwendung zur Verfügung stehen

Bis jetzt haben wir einen Service definiert und diesen mit dem Angular-Modul registriert, nun wollen wir den Service in unserer Komponente nutzen.

app.component.ts
 1 import { Component } from '@angular/core';
 2 import { DataService } from './data.service';
 3 
 4 @Component({
 5   selector: 'app-root',
 6   template: '<div>Hello World!</div>'
 7 })
 8 export class AppComponent {
 9   constructor(dataService: DataService) {
10     console.log(dataService.getData());
11   }
12 }

Erklärung:

  • Zeile 9: Hier definieren wir den “DataService” als Abhängigkeit unserer Komponente. Zur Laufzeit wird die Konstruktorfunktion eine Instanz des “DataService” erhalten
  • Zeile 10: Hier nutzen wir die getData-Methode der dataService-Instanz. Statt die Daten in der Konstruktorfunktion zu holen, ist es besser die Daten in der ngOnInit-Methode zu holen. Die ngOnInit-Methode wird im Rezept “Code ausführen bei der Initialisierung einer Komponente” gezeigt

Diskussion

Nach unserer kurzen Erklärung ist anzunehmen, dass zur Lösung noch einige Fragen offen sind. Was genau sind “providers”? Warum brauchen wir kein “new”, um den DataService zu instantiieren? Wir haben erwähnt, dass wir den Service als “Injectable” definieren. Aber was heißt das? Diese Fragen werden wir jetzt beantworten.

Dependency Injection

Angular nutzt Dependency Injection (DI), um Abhängigkeiten zu verwalten. Alle Provider werden mit dem sogenannten “Injector” registriert. Erst die Nutzung einer TypeScript-Klasse in einem Provider macht die Klasse zu einem Service.

Als Erstes wollen wir die Frage “Was genau sind ‘providers’?” beantworten. Kurz gesagt ist ein “Provider” ein Rezept, um für einen Konstruktorparameter einen Wert bereitzustellen. So ein Rezept besteht aus zwei Zutaten, ein Token und die Information welcher Wert zur Laufzeit übergeben werden soll. In unserem Beispiel sagt das Rezept dem Injector, dass ein Parameter vom Typ “DataService” (das Token) eine Instanz (der Wert) der DataService-Klasse braucht. Wir haben die Kurzform für einen Provider benutzt und in diesem Fall wird immer für die Klasse (das Token), die der providers-Array übergeben wird, eine Instanz erzeugt. Das beantwortet auch gleich die Frage “Warum brauchen wir kein ‘new’, um den “DataService” zu instantiieren?”. Der Injector tut das für uns. Das hat den Vorteil, dass wir als Nutzer des Services nicht wissen müssen, wie wir diesen instantiieren müssen. Ein weitere Vorteil ist, dass wir z. B. beim Testen für das “DataService”-Token einen anderen Wert übergeben können. Z. B. eine Klasse die ein “DataService”-Mock erzeugt.

Wie wir schon wissen, werden Typinformationen zur Compile-Zeit entfernt. Damit der Injector trotzdem weiß was für ein Wert ein Konstruktorparameter erwartet, wird diese Information in den Metadaten der Komponente gespeichert. Da Klassen keine Metadaten besitzen, müssen wir eine Klasse, die wir als Service benutzen wollen mit Hilfe des Injectable-Decorators als “Injectable” definieren. Somit erhält auch unser Service-Klasse Metadaten und kann dann Abhängigkeiten haben, die in den Metadaten gespeichert werden. Kurz gesagt ist ein “Injectable” Service ein Service, der Abhängigkeiten als Konstruktorparameter besitzen kann. Eigentlich brauchen wir den Injectable-Decorator nur, wenn wir für einen Service Abhängigkeiten als Konstruktorparameter definieren wollen. Obwohl unser Service keine Abhängigkeiten hat, haben wir den Decorator verwendet, damit zum Einen alle Services einheitlich sind und zum Anderen Fehler vermieden werden, falls wir später doch eine Abhängigkeit brauchen. Wir wissen jetzt also, was es bedeutet, einen Service als “Injectable” zu definieren und warum wir das machen.

Wir haben unsere Diskussion möglichst kurz gehalten. Eine ausführlichere Erklärung, wie Dependency Injection in Angular funktioniert und was passiert, wenn wir eine Klasse in mehrere providers-Array nutzen, würde den Rahmen eines Rezepts sprengen. Eine vollständige Erklärung, wie Dependency Injection funktioniert und was wir alles damit machen können, gibt es auf der Angular 2 Webseite: Dependency Injection und Hierarchical Dependency Injectors. Dort wird auch beschrieben, wie wir komplexere Provider-Rezepte nutzen können und was wir außer Services noch als Abhängigkeiten definieren können.

Code

Code auf Github

Live Demo auf angular2kochbuch.de

Weitere Ressourcen

  • Neue Services können wir auch mit Hilfe des generate-Kommandos von angular-cli generieren. Mehr Informationen gibt es in Appendix-B: angular-cli

Angular 2 in Produktion nutzen

Problem

Ich möchte meine Angular 2 Anwendung produktiv nutzen.

Zutaten

Lösung

Mit angular-cli ist es sehr einfach eine produktiv-Version von unserer Anwendung zu bauen. Tatsächlich hat ein neu initialisiertes Projekt schon den Code in der main.ts-Datei, den wir in diesem Rezept hinzufügen wollen. Auch die nötige environment-Dateien ist da schon vorhanden.

Als Erstes schauen wir uns die environment.prod.ts-Datei an. Diese befindet sich im Verzeichnis “src/environments”.

environment.prod.ts
1 export const environment = {
2     production: true
3 };

Erklärung:

Bei einem angular-cli-Projekt befinden sich im Verzeichnis “src/environments” zwei Dateien. Die eine mit Namen “environment.ts” und eine mit Namen “environment.prod.ts”. Eigentlich sind beide Dateien optional. Diese werden nur gebraucht, wenn wir sie in unserem Code referenzieren. Der Unterschied zwischen den zwei Dateien, ist der Wert für die production-Eigenschaft. Dieser ist true in der environment.prod.ts-Datei und false in der environment.ts-Datei.

Jetzt sehen wir warum es sinnvoll sein kann die environment-Dateien zu referenzieren.

main.ts
 1 import './polyfills.ts';
 2 
 3 import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
 4 import { enableProdMode } from '@angular/core';
 5 import { environment } from './environments/environment';
 6 import { AppModule } from './app/';
 7 
 8 if (environment.production) {
 9   enableProdMode();
10 }
11 
12 platformBrowserDynamic().bootstrapModule(AppModule);

Erklärung:

  • Zeile 5: Importiert eine environment-Datei. Ob es die environment.ts- oder die environment.prod.ts-Datei ist, wird von angular-cli zur Bauzeit definiert
  • Zeilen 8-10: Hier wird der Produktions-Modus von Angular aktiviert aber nur, wenn angular-cli die environment.prod.ts-Datei nutzt

Als letztes müssen wir unser Projekt mit der --prod-Option bauen.

1 ng build --prod

Sobald die --prod-Option benutzt wird, nutzt angular-cli die environment.prod.ts-Datei. In allen anderen Fällen z. B. beim ng serve wird die environment.ts-Datei benutzt.

Code

Code auf Github

Live Demo auf angular2kochbuch.de

Rezepte, um mit der Anzeige zu interagieren

In diesem Kapitel geht es um die verschiedenen Möglichkeiten, die uns Angular anbietet, um Daten einer Komponente in der View anzuzeigen. Des Weiteren zeigen wir Möglichkeiten, CSS-Klassen und Styles abhängig von den Daten, die in unserer Komponente liegen, zu ändern. Auch das Ein- und Ausblenden von Teilen des DOMs abhängig von einer Bedingung wird hier gezeigt. Ferner werden wir sehen, wie wir auf Nutzer-Input wie z. B. auf Klicks reagieren können.

Daten einer Komponente in der View anzeigen

Problem

Ich möchte Daten, die sich in meinem TypeScript-Code befinden, in der View anzeigen, damit der Nutzer diese sehen kann.

Zutaten

Lösung 1

app.component.ts
 1 ...
 2 
 3 @Component({
 4   selector: 'app-root',
 5   template: `
 6     <div>Hello World!</div>
 7     <div>My name is {{name}}</div>
 8   `
 9 })
10 export class AppComponent {
11   name: string;
12 
13   constructor() {
14     this.name = 'Max';
15   }
16 }
17 ...

Erklärung:

Um Daten anzuzeigen, müssen wir zwei Sachen machen: Erstens müssen wir dem Angular-Template sagen, welche Variablen es anzeigen soll. Zweitens müssen wir diese Variablen in unserer Klasse als Eigenschaften definieren. Um den Code übersichtlicher zu gestalten, nutzen wir hier für die template-Eigenschaft Backticks (`) statt Anführungszeichen ('). Das ermöglicht uns, das Template in mehrere Zeilen aufzuspalten, ohne mehrere Strings mittels Pluszeichen konkatenieren zu müssen.

  • Zeile 7: Hier teilen wir Angular mit, das “name” interpoliert werden soll
  • Zeile 11: Typdefinition der Komponenten-Eigenschaft
  • Zeile 14: Wertzuweisung der name-Eigenschaft. Wichtig ist, dass der Name der Eigenschaft im Template genauso wie in der Klasse geschrieben wird

Lösung 2

app.component.ts
 1 ...
 2 
 3 @Component({
 4   selector: 'app-root',
 5   template: `
 6     <div>Hello World!</div>
 7     <div>My last name is {{lastname}}</div>
 8   `
 9 })
10 export class AppComponent {
11   lastname = 'Mustermann';
12 }

Erklärung:

In dieser Lösung wird “lastname” nicht im Konstruktor initialisiert, sondern in der Klasse (Zeile 11). Siehe auch TypeScript-Klassen.

Diskussion

Das Beispiel ist sehr einfach gehalten. Die zweite Lösung benötigt weniger Code aber es ist Geschmackssache welche der beiden Varianten wir benutzen. Von der Funktionalität her sind beide gleich. Ein Beispiel mit beiden Schreibweisen gibt es im Code-Beispiel auf Github.

Code

Code auf Github

Live Demo auf angular2kochbuch.de

Weitere Ressourcen

Liste von Daten anzeigen

Problem

Ich hab eine Liste von Benutzerdaten und möchte diese in meine View anzeigen.

Zutaten

Lösung

app.component.ts
 1 import { Component } from '@angular/core';
 2 
 3 interface User {
 4   firstname: string;
 5   lastname: string;
 6 }
 7 
 8 const users: Array<User> = [
 9   { firstname: 'Max', lastname: 'Mustermann' },
10   { firstname: 'John', lastname: 'Doe' }
11 ];
12 
13 @Component({
14   selector: 'app-root',
15   template: `
16     <ul>
17       <li *ngFor="let user of users">
18         Name: {{user.firstname}} {{user.lastname}}
19       </li>
20     </ul>
21   `
22 })
23 export class AppComponent {
24   users: Array<User>;
25 
26   constructor() {
27     this.users = users;
28   }
29 }

Erklärung:

  • Zeilen 3-6: Interfacedefinition für User-Objekte
  • Zeile 17: Nutzung der NgFor-Direktive, um eine Liste anzuzeigen
  • Zeile 18: Hier nutzen wir die lokale Variable “user”, um Informationen anzuzeigen, wie wir es im Rezept “Daten einer Komponente in der View anzeigen” getan haben

Diskussion

Es gibt noch weitere mögliche Schreibweisen für das Anzeigen einer Liste von Daten. Diese hier ist die Kürzeste und auch vermutlich die Einfachste. Die restlichen Varianten sind im Github Code-Beispiel zu finden. Von der Funktionalität her sind alle Varianten gleich.

Erklärung zu der ngFor-Syntax:

Der Stern (*) vor dem ngFor ist essentiell und Teil der Syntax. Er zeigt an, dass der li-Tag und alle Elemente, die der Tag beinhaltet, als Template für die Instanz der NgFor-Direktive benutzt werden sollen. Der Teil nach dem of ist der Name der Komponenten-Eigenschaft, die unsere Liste referenziert. Das Keyword let definiert eine lokale Template-Eingabevariable für die Instanz der NgFor-Direktive. Diese können wir nur innerhalb des Elementes mit dem ngFor nutzen. Sie hält eine Referenz auf das aktuelle Objekt in der Array.

Code

Code auf Github

Live Demo auf angular2kochbuch.de

Weitere Ressourcen

  • Offizielle NgFor-Dokumentation auf der Angular 2 Webseite
  • Weitere Informationen zu lokalen Variablen und der Template-Syntax gibt es in Appendix A: Template-Syntax

Auf Nutzer-Input reagieren

Problem

Ich möchte in meiner Komponente eine Methode aufrufen, wenn der Nutzer einen Browser-Event wie z. B. “click” auslöst.

Zutaten

  • Angular 2 Anwendung
  • Ein Browser-Event, wir nutzen hier “click” als Beispiel
  • Methode, die aufgerufen werden soll, wenn der Nutzer auf das Element klickt

Lösung 1

app.component.ts
 1 ...
 2 
 3 @Component({
 4   selector: 'app-root',
 5   template: '<div (click)="clicked()">Click me</div>'
 6 })
 7 export class AppComponent {
 8   clicked() {
 9     console.log('Clicked');
10   }
11 }

Erklärung:

  • Zeile 5: Hier findet eine Event-Bindung statt. In diesem Fall wird das click-Event gebunden
  • Zeilen 8-10: Die Methode, die aufgerufen werden soll, wenn der Nutzer auf das Element klickt. Zu beachten ist, dass der Name der Methode identisch zu dem Namen, den wir im Template nutzen, sein muss

Lösung 2

app.component.ts
 1 ...
 2 
 3 @Component({
 4   selector: 'app-root',
 5   template: '<div on-click="clicked()">Click me</div>'
 6 })
 7 export class AppComponent {
 8   clicked() {
 9     console.log('Clicked');
10   }
11 }

Erklärung:

Das ist eine alternative Schreibweise zu der Schreibweise in Lösung 1. Statt Klammern für den Event-Namen nutzen wir hier “on-“ als Präfix. Die Funktionalität bleibt dabei jedoch gleich.

Diskussion

Die Event-Bindung ersetzt alle Event-Direktiven, die es in Angular 1.x gibt wie z. B. “ng-click”, “ng-keypress” und “ng-keydown”. Wir haben im Beispiel “click” benutzt, aber wir hätten auch andere Event-Namen zwischen den Klammern schreiben können wie z. B. “keypress”. Allgemein ist der Namen zwischen den Klammern der Namen des Events, auf das wir reagieren möchten. Nach dem Gleichheitszeichen kommt die Aktion, die als Reaktion zum Event ausgeführt werden soll. Die Event-Bindung ist eine Form der Datenbindung.

Code

Code auf Github

Live Demo auf angular2kochbuch.de

Weitere Ressourcen

CSS-Klassen auf Basis von booleschen Werten setzen/entfernen

Problem

Ich möchte anhand eines booleschen Wertes definieren, wann eine CSS-Klasse gesetzt wird und wann nicht.

Zutaten

Lösung 1

app.component.ts
 1 import { Component } from '@angular/core';
 2 
 3 @Component({
 4   selector: 'app-root',
 5   styles: [
 6     `.box {
 7         width: 100px;
 8         height: 100px;
 9         border: 1px solid black;
10     }`,
11     '.red { background-color: red; }',
12     '.green { background-color: green; }'
13   ],
14   template: `
15     <div class="box" [ngClass]="{red: box.isRed}"></div>
16     <div class="box green" [ngClass]="{green: box.isGreen}"></div>
17   `
18 })
19 export class AppComponent {
20   box = {
21     isRed: true,
22     isGreen: false
23   };
24 }

Erklärung:

  • Zeilen 6-12: Definition der CSS-Klassen, die wir benötigen
  • Zeilen 15-16: Zwei div-Tags mit CSS-Klassen. Initiale CSS-Klassen werden über das class-Attribut gesetzt. Dynamische CSS-Klassen werden mit Hilfe der ngClass-Eigenschaft gesetzt. Die Eigenschaft bekommt als Wert ein Objekt, dessen Keys die CSS-Klassen sind, die dynamisch hinzugefügt und entfernt werden
    • Zeile 15: Durch die input-Eigenschaft “ngClass” wird die CSS-Klasse “red” gesetzt, weil die isRed-Eigenschaft des box-Objektes true ist
    • Zeile 16: Durch die input-Eigenschaft “ngClass” wird die CSS-Klasse “green” entfernt, weil die isGreen-Eigenschaft des box-Objektes false ist
  • Zeilen 20-23: Objekt mit booleschen Werten, die benutzt werden, um CSS-Klassen im Template hinzuzufügen bzw. zu entfernen

Lösung 2

Wir haben bereits in Lösung 1 gesehen, dass die ngClass-Eigenschaft ein Objekt mit CSS-Klassen als Keys und true/false als Werten bekommt. Statt das Objekt im Template zu definieren, können wir es auch in unserer Klasse definieren.

app.component.ts
 1 import { Component } from '@angular/core';
 2 
 3 @Component({
 4   selector: 'app-root',
 5   styles: [
 6     `.box {
 7         width: 100px;
 8         height: 100px;
 9         border: 1px solid black;
10     }`,
11     '.red { background-color: red; }'
12   ],
13   template: '<div [ngClass]="classes"></div>'
14 })
15 export class AppComponent {
16   classes = {
17     red: true,
18     box: true
19   };
20 }

Erklärung:

  • Zeilen 6-11: Definition der CSS-Klassen, die wir benötigen
  • Zeile 13: div-Tag mit ngClass-Eigenschaft, die auf die classes-Eigenschaft der Klasse zugreift
  • Zeilen 16-19: Objekt mit CSS-Klassen als Keys und boolesche Werte die angeben, ob die CSS-Klassen gesetzt werden oder nicht

Diskussion

Um das Beispiel möglichst klein zu halten, haben wir hier auf das dynamische Verändern der CSS-Klassen verzichtet. Im Github Code-Beispiel wird gezeigt, wie wir mittels “click” die CSS-Klassen für unsere div-Tags entfernen und hinzufügen können. Um das Code-Beispiel zu verstehen, wird zusätzlich das Rezept “Auf Nutzer-Input reagieren” benötigt.

Wir nutzen hier eine Datenbindung mit eckigen Klammern ([…]). Diese Art der Datenbindung wird Eigenschafts-Bindung genannt. Falls wir nur eine einzige Klasse nutzen, können wir auch eine Klassen-Bindung dafür nutzen.

Code

Code auf Github für die erste Lösung

Live Demo für die erste Lösung auf angular2kochbuch.de

Code auf Github für die zweite Lösung

Live Demo für die zweite Lösung auf angular2kochbuch.de

Weitere Ressourcen

Teile der View konditional mit NgIf anzeigen

Problem

Ich möchte Teile der View nur dann anzeigen, wenn eine bestimmte Bedingung erfüllt ist.

Zutaten

Lösung

app.component.ts
 1 import { Component } from '@angular/core';
 2 
 3 @Component({
 4   selector: 'app-root',
 5   template: `
 6     <div>Hello World!</div>
 7     <div *ngIf="showName">
 8       <p>My name is Max</p>
 9     </div>
10   `
11 })
12 export class AppComponent {
13   showName: boolean;
14 
15   constructor() {
16     this.showName = true;
17   }
18 }

Erklärung:

  • Zeile 7: Nutzung der NgIf-Direktiven, um den div-Tag nur dann im DOM zu haben, wenn “showName” den Wert true hat
  • Zeile 13: Definition der showName-Eigenschaft mit Typ “boolean”
  • Zeile 16: Standardmäßig soll die showName-Eigenschaft den Wert true besitzen (div-Tag ist im DOM)

Diskussion

Um das Beispiel möglichst klein zu halten, haben wir hier auf das dynamische Verändern des Wertes der showName-Eigenschaft verzichtet. Im Github Code-Beispiel wird gezeigt wie wir mittels “click” den Wert verändern können. Dort können wir auch sehen, wie sich die View verändert, je nachdem, ob “showName” den Wert true oder false hat.

Es gibt noch weitere mögliche Schreibweisen für das konditionale Anzeigen von Teilen der View mittels der NgIf-Direktive. Die hier verwendete ist die kürzeste und vermutlich die einfachste. Weitere Schreibweisen sind im Github Code-Beispiel zu finden. Von der Funktionalität her sind alle Varianten gleich.

Erklärung zu der ngIf-Syntax

Der Stern (*) vor dem ngIf ist essentiell und Teil der Syntax. Er zeigt an, dass der div-Tag und alle Elemente, die der Tag beinhaltet, als Template für die Instanz der NgIf-Direktiven benutzt werden sollen. Nach *ngIf= kommt ein Angular-Template-Ausdruck, der die Bedingung angibt. Wenn die Evaluation des Ausdruckes true zurückgibt, ist die Bedingung wahr und das Template wird angezeigt. Andernfalls wird das Template aus dem DOM entfernt. Wir haben hier einen sehr einfachen Ausdruck benutzt. Wir hätten auch einen komplexeren Ausdruck nutzen können, z. B. einen, der einen Vergleich mit === beinhaltet.

Code

Code auf Github

Live Demo auf angular2kochbuch.de

Weitere Ressourcen

Teile der View konditional mit NgSwitch anzeigen

Problem

Ich möchte unterschiedliche Teile der View anzeigen, je nach Wert eines Angular-Ausdrucks.

Zutaten

  • Angular 2 Anwendung
  • Die NgSwitch-Direktive von Angular
  • Die NgSwitchCase-Direktive von Angular
  • Die NgSwitchDefault-Direktive von Angular

Lösung

app.component.ts
 1 import { Component } from '@angular/core';
 2 
 3 @Component({
 4   selector: 'app-root',
 5   template: `
 6     <div [ngSwitch]="color">
 7       <p>What color are you?</p>
 8       <p *ngSwitchCase="'blue'">I am blue</p>
 9       <p *ngSwitchCase="'red'">I am red</p>
10       <p *ngSwitchDefault>Color not known</p>
11     </div>
12   `
13 })
14 export class AppComponent {
15   color: string;
16 
17   constructor() {
18     this.color = 'blue';
19   }
20 }

Erklärung:

  • Zeile 6: Nutzung der NgSwitch-Direktive mit der color-Eigenschaft der Komponente
  • Zeilen 7-10: Inhalt der NgSwitch-Direktiven
    • Zeile 7: Wird immer angezeigt unabhängig vom Wert von “color”
    • Zeile 8: Wird nur dann angezeigt, wenn “color” den Wert 'blue' hat
    • Zeile 9: Wird nur dann angezeigt, wenn “color” den Wert 'red' hat
    • Zeile 10: Wird nur dann angezeigt, wenn “color” irgendeinen Wert außer 'blue' und 'red' hat
  • Zeile 18: Standardmäßig ist der Wert von “color” 'blue'

Diskussion

Die NgSwitch-Direktive ist vergleichbar mit einer JavaScript switch-Anweisung. Bei der Nutzung im Template erhält sie über ngSwitch (input-Eigenschaft der NgSwitch-Direktive) einen Angular-Template-Ausdruck, der dann ausgewertet wird. In unserem Beispiel besteht der Ausdruck aus der color-Eigenschaft. Diese Auswertung wird dann mit jedem Ausdruck der NgSwitchWhen-Direktiven verglichen. Angular nutzt für den Vergleich ===. In unserem Beispiel haben wir 'blue' und 'red' als Ausdrücke für NgSwitchCase benutzt. Wenn der Vergleich den Wert true zurück gibt, wird der Tag mit der Direktiven und dessen Inhalt angezeigt. Wenn kein Vergleich true zurück gibt, wird der Tag mit der NgSwitchDefault-Direktiven und dessen Inhalt angezeigt. Tags, die weder eine NgSwitchCase- noch eine NgSwitchDefault-Direktive nutzen, werden immer angezeigt.

Um das Beispiel möglichst klein zu halten, haben wir hier auf das dynamische Verändern des Wertes der color-Eigenschaft verzichtet. Im Github Code-Beispiel wird gezeigt, wie wir mittels “click” den Wert verändern können. Dort können wir auch sehen, wie sich die View in Abhängigkeit des Wertes der color-Eigenschaft verändert.

Es gibt noch eine weitere mögliche Schreibweise für die NgSwitchCase- und NgSwitchDefault-Direktiven. Diese hier ist die kürzeste und vermutlich die einfachste. Die zweite Schreibweise ist im Github Code-Beispiel zu finden. Von der Funktionalität her sind beide Schreibweisen gleich.

Erklärung zur ngSwitchWhen- und ngSwitchDefault-Syntax

Der Stern (*) vor dem ngSwitchWhen und ngSwitchDefault ist essentiell und Teil der Syntax. Er zeigt an, dass die p-Tags und alle Elemente, die die Tags beinhalten, als Template für die jeweilige Instanz der NgSwitchCase- bzw. der NgSwitchDefault-Direktiven benutzt werden sollen. Nach *ngSwitchCase= kommt ein Angular-Template-Ausdruck. Dieser Ausdruck verglichen mit der Auswertung des NgSwitch-Ausdrucks gibt an, wann das Template angezeigt werden soll. Wenn der Vergleich true ergibt, wird das Template angezeigt. Wenn der false ergibt, wird das Template aus dem DOM entfernt. Das NgSwitchDefault-Template wird nur dann angezeigt, wenn alle Vergleiche false ergeben.

Code

Code auf Github

Live Demo auf angular2kochbuch.de

Weitere Ressourcen

Styles eines Elements dynamisch verändern

Problem

Ich möchte die Größe (height/width) eines Elements durch Werte in meiner Komponente definieren. Eine Änderung der Werte in der Komponente soll auch die Größe des Elements verändern.

Zutaten

  • Angular 2 Anwendung
  • Eigenschaften in der Komponente, die wir im Template referenzieren
  • NgStyle-Direktive von Angular

Lösung 1

app.component.ts
 1 import { Component } from '@angular/core';
 2 
 3 @Component({
 4   selector: 'app-root',
 5   template: `
 6     <div [ngStyle]="{'width': elemWidth, 'height': elemHeight}"
 7         style="background-color: red"></div>
 8   `
 9 })
10 export class AppComponent {
11   elemWidth: string;
12   elemHeight: string;
13   constructor() {
14     this.elemWidth = '100px';
15     this.elemHeight = '100px';
16   }
17 }

Erklärung:

  • Zeilen 6-7: div-Tag mit Styles. Statische Styles werden über das style-Attribut gesetzt. Dynamische Styles werden mit Hilfe der ngStyle-Eigenschaft, einer input-Eigenschaft der NgStyle-Direktive, gesetzt. Die Eigenschaft erhält als Wert ein Objekt dessen Keys die style-Eigenschaften sind, die gesetzt werden (hier width und height). Die Werte für die Styles sind in der Klasse der Komponente definiert (siehe Zeilen 14 und 15)
  • Zeile 14: Der Wert für die Breite des Elements
  • Zeile 15: Der Wert für die Höhe des Elements

Lösung 2

Wir haben bereits in Lösung 1 gesehen, dass die ngStyle-Eigenschaft ein Objekt mit style-Eigenschaften als Keys und Werten für die Styles als Werte für die Keys bekommt. Statt Werte individuell in der Klasse zu definieren, können wir auch direkt das Objekt für die ngStyle-Eigenschaft definieren.

app.component.ts
 1 import { Component } from '@angular/core';
 2 
 3 interface Dimensions {
 4   width: string;
 5   height: string;
 6 }
 7 
 8 @Component({
 9   selector: 'app-root',
10   template: `
11     <div [ngStyle]="dimensions" style="background-color: red"></div>
12   `
13 })
14 export class AppComponent {
15   dimensions: Dimensions;
16   constructor() {
17     this.dimensions = {
18       width: '100px',
19       height: '100px'
20     };
21   }
22 }

Erklärung:

  • Zeile 11: div-Tag mit ngStyle-Eigenschaft, die auf die dimensions-Eigenschaft der Klasse zugreift
  • Zeilen 17-20: Objekt mit style-Eigenschaften als Keys und Werten, die die Werte für die Styles definieren

Diskussion

Um das Beispiel möglichst klein zu halten, haben wir hier auf das dynamische Verändern der Werte der style-Eigenschaften verzichtet. Im Github-Code-Beispiel wird gezeigt, wie wir mittels “click” die Werte der styles-Eigenschaften verändern können. Um das Code-Beispiel zu verstehen, wird auch das Rezept “Auf Nutzer-Input reagieren” benötigt.

Wir nutzen hier eine Datenbindung mit eckigen Klammern ([…]). Diese Art der Datenbindung wird Eigenschafts-Bindung genannt. Falls wir nur eine einzige styles-Eigenschaft setzen, können wir auch eine Style-Bindung dafür nutzen.

Code

Code auf Github für die erste Lösung

Live Demo für die erste Lösung auf angular2kochbuch.de

Code auf Github für die zweite Lösung

Live Demo für die zweite Lösung auf angular2kochbuch.de

Weitere Ressourcen

Rezepte für Formulare

Angular bietet uns mehrere Möglichkeiten, Formulare zu implementieren. Wir können in Angular 2 zwischen zwei Arten von Formularen unterscheiden: “Template-Driven Forms” und “Model-Driven Forms” auch bekannt als “Reactive Forms”. Beide Formulararten bieten die gleichen Funktionalität an. Nur der Weg, den wir gehen müssen, um die Funktionalität zu implementieren ist anders. Die Art des Formulars steht im Titel des Rezepts. TDF für “Template-Driven” und MDF für “Model-Driven”.

Bei den “Template-Driven Forms” befindet sich ein Großteil der Logik, wie z. B. die Validierung, im Template. Auch die Synchronisation zwischen den Daten, die der Nutzer sieht und den Daten in der Klasse der Komponente (dem Modell) wird im Template definiert. So werden Formulare auch bereits in Angular 1.x implementiert, wo wir direkt im Template z. B. mit dem required-Attribut definieren, dass das Feld ein Pflichtfeld ist. Da “Template-Driven Forms” einfacher und in der Regel mit weniger Code zu implementieren sind, werden wir uns zuerst mit diesen beschäftigen. Allerdings haben diese Formulare auch Nachteile. Da sich die Logik im Template befindet, ist es schwer bis unmöglich, diese in Unit-Tests zu testen. Des Weiteren kann das Template bei Formularen mit komplexer Validierung sehr schnell unübersichtlich werden.

Bei den “Model-Driven Forms” befindet sich die meiste Logik in der Klasse der Komponente. Das Template hat nur die nötigen Informationen, um die Formular-Felder mit dem Formular-Modell zu verbinden. Als Formular-Modell bezeichnen wir in diesem Zusammenhang die Implementierung des Formulars in der Klasse der Komponente. “Model-Driven Forms” benötigen in der Regel mehr Code. Wir müssen das Formular im Template implementieren und dann die Logik in der Klasse. Da die Logik sich in der Klasse befindet, können wir die Komponente gut Unit-testen.

Wir werden jetzt mit einem einfachen “Template-Driven” Formular anfangen und werden uns danach “Model-Driven” Formulare anschauen.

TDF: Ein einfaches Formular implementieren

Problem

Ich möchte Daten vom Benutzer bekommen und brauche dafür ein einfaches Formular.

Zutaten

  • Angular 2 Anwendung
  • Forms-Modul von Angular
  • NgModel-Direktive
  • NgForm-Direktive mit dem ngSubmit-Event
  • Anpassungen an der app.module.ts-Datei
  • Anpassungen an der package.json-Datei

Lösung

app.component.ts
 1 import { Component } from '@angular/core';
 2 
 3 @Component({
 4   selector: 'app-root',
 5   template: `
 6     <form (ngSubmit)="onSubmit()">
 7       <label>Username
 8         <input type="text"
 9           [(ngModel)]="user.username"
10           name="username" />
11       </label>
12       <label>Password
13         <input type="password"
14           [(ngModel)]="user.password"
15           name="password" />
16       </label>
17       <button type="submit">Submit</button>
18     </form>
19   `
20 })
21 export class AppComponent {
22   user = {
23     username: '',
24     password: ''
25   };
26 
27   onSubmit() {
28     console.log(this.user);
29   }
30 }

Erklärung:

  • Zeilen 6-18: Das Formular
    • Zeile 6: Wir binden das ngSubmit-Event des Formulars an unsere onSubmit-Methode
    • Zeilen 8-10: Eingabefeld für den Benutzernamen
      • Zeile 9: Hier nutzen wir die NgModel-Direktive, um die View mit den Daten (dem Modell) der Komponente zu verbinden. Konkreter reden wir hier von einer beidseitigen Bindung zwischen dem Wert des Eingabefeldes und der username-Eigenschaft des user-Objekts (siehe Zeilen 22-25)
      • Zeile 10: Hier definieren wir einen Namen für das Eingabefeld
    • Zeile 13-15: Ähnlich wie Zeilen 8-10, aber für das Passwort-Feld
  • Zeilen 22-25: Ein Objekt, in welchem die Daten, die der Nutzer in das Formular eingibt, gespeichert werden. Die leeren Strings für die Eigenschaften “username” und “password” sind die Default-Werte für die Eingabefelder
  • Zeilen 27-29: Methode, die aufgerufen wird, wenn der Nutzer ein submit-Event auslöst, z. B. durch ein Klick auf den Button (siehe auch Zeile 6)

Da sich Formular-Direktiven wie z. B. “NgModel” in einem eigenen Angular-Modul befinden, müssen wir dieses Modul in unser “AppModule” importieren.

app.module.ts
 1 import { NgModule }      from '@angular/core';
 2 import { BrowserModule } from '@angular/platform-browser';
 3 import { FormsModule } from '@angular/forms';
 4 
 5 import { AppComponent }  from './app.component';
 6 
 7 @NgModule({
 8   imports: [ BrowserModule, FormsModule ],
 9   declarations: [ AppComponent ],
10   bootstrap: [ AppComponent ]
11 })
12 export class AppModule { }

Erklärung:

  • Zeile 8: Hier importieren wir das “FormsModule” in unser Modul. In diesem Modul befinden sich alle Direktiven, die wir für Template-Driven Forms brauchen

Da sich das “FormsModule” in einem eigenen npm-Paket befindet, müssen wir dieses auch in der package.json deklarieren.

package.json
1 {
2   ...
3   "dependencies": {
4     ...
5     "@angular/forms": "2.1.2"
6     ...
7   }
8   ...
9 }

Wenn eine Angular-Anwendung mit angular-cli initialisiert wird, wird das “FormsModule” automatisch von angular-cli importiert und das entsprechende npm-Paket in der package.json-Datei deklariert.

Diskussion

Jedes form-Tag bekommt automatisch eine Instanz der NgForm-Direktive. Ein Formular hat von sich aus kein ngSubmit-Event, sondern ein submit-Event. Da aber das Formular auch eine Instanz der NgForm-Direktive ist, haben wir Zugriff auf das ngSubmit-Event der Direktive. Das ngSubmit-Event ist also eine output-Eigenschaft der NgForm-Direktive. Im Grunde genommen bindet die NgForm-Direktive das submit-Event des Formulars und leitet es an das ngSubmit-Event weiter. Der einzige Unterschied zwischen den zwei Events ist, dass ngSubmit die submitted-Eigenschaft des Formulars (NgForm-Instanz) auf true setzt.

Die NgModel-Direktive definiert ein sogenanntes “Control” und bindet ein Modell in der Komponenten-Klasse mit einem Eingabefeld/Select usw. in der View. Ein Control kann dann entweder innerhalb eines form-Tags oder ohne form-Tag (standalone Control) benutzt werden. Wenn wir die NgModel-Direktive innerhalb eines form-Tags nutzen, müssen wir für das Element mit der Direktive auch das name-Attribut definieren. Das name-Attribut wird benutzt, um das Control mit der Instanz der ngForm-Direktive zu registieren.

Wie schon erwähnt, nutzen wir in den Zeilen 9 und 14 eine beidseitige Bindung. Wir hätten die beidseitige Bindung auch in eine Eigenschaft- und eine Event-Bindung aufspalten können. Wie das aussieht wird in Appendix A gezeigt. Da die Nutzung der beidseitigen Bindung einfacher ist, werden wir sie auch in weiteren Formular-Rezepten nutzen.

Code

Code auf Github

Live Demo auf angular2kochbuch.de

Weitere Ressourcen

  • Offizielle NgModel-Dokumentation auf der Angular 2 Webseite
  • Offizielle NgForm-Dokumentation auf der Angular 2 Webseite
  • Weitere Informationen zu Event-, Eigenschafts- und beidseitigen Bindungen gibt es in Appendix A: Template-Syntax

TDF: Gültigkeit eines Formulars überprüfen

Problem

Ich möchte, dass ein Submit nur dann möglich ist, wenn das Formular gültig ist. Ein Formular ist nur dann gültig, wenn alle seine Eingabefelder gültig sind.

Zutaten

  • Ein Formular
  • Validierungs-Attribute
  • Anpassungen am Formular
  • Lokale (Template-) Variable, die das Formular referenziert

Lösung 1

In dieser Lösung werden wir sehen, wie wir die Gültigkeit des Formulars im Template überprüfen können. Wir werden den Submit-Button deaktivieren, wenn das Formular ungültig ist. Dadurch wird das Submit-Event unterbunden, solange nicht alle Formular-Felder gültig sind.

app.component.ts
 1 import { Component } from '@angular/core';
 2 
 3 @Component({
 4   selector: 'app-root',
 5   template: `
 6     <form (ngSubmit)="onSubmit()" #form="ngForm" novalidate>
 7       <label>Username
 8         <input name="username" type="text" [(ngModel)]="user.username"
 9           required />
10       </label>
11       <label>Password
12         <input name="password" type="password" [(ngModel)]="user.password"
13           required
14           minlength="10" />
15       </label>
16       <button type="submit" [disabled]="form.invalid">Submit</button>
17     </form>
18   `
19 })
20 
21 ...

Erklärung:

  • Zeile 6: Mit #form="ngForm" definieren wir eine lokale (Referenz-) Variable namens “form”. Die Variable ist eine Referenz auf die Instanz der NgForm-Direktive unseres Formulars. Wir nutzen auch das novalidate-Attribut, damit der Browser keine Fehlermeldungen für ungültige Eingabefelder anzeigt. Dies ist kein Angular-spezifisches Attribut
  • Zeilen 8-9: Eingabefeld für den Benutzernamen
    • Zeile 9: Mittels required definieren wir das Eingabefeld als Pflichtfeld
  • Zeilen 12-14: Eingabefeld für das Passwort
    • Zeile 13: Mittels required definieren wir das Eingabefeld als Pflichtfeld
    • Zeile 14: Mittels minlength="10" definieren wir, dass das Passwort mindestens zehn Zeichen lang sein muss
  • Zeile 16: Hier binden wir die disabled-Eigenschaft an den Ausdruck form.invalid. Wenn das Formular ungültig ist, wird der Button deaktiviert sein

Lösung 2

In dieser Lösung werden wir sehen, wie wir die Gültigkeit des Formulars in der Klasse überprüfen können. Wir nutzen das Formular aus der ersten Lösung mit zwei Änderungen: Wir übergeben der onSubmit-Methode die lokale Variable “form” und wir nutzen nicht mehr die disabled-Eigenschaft des Buttons.

app.component.ts
 1 import { Component } from '@angular/core';
 2 import { NgForm } from '@angular/forms';
 3 
 4 @Component({
 5   selector: 'app-root',
 6   template: `
 7     <form (ngSubmit)="onSubmit(form)" #form="ngForm" novalidate>
 8       <label>Username
 9         <input type="text"
10           name="username"
11           [(ngModel)]="user.username"
12           required />
13       </label>
14       <label>Password
15         <input type="password"
16           name="password"
17           [(ngModel)]="user.password"
18           required minlength="10" />
19       </label>
20       <button type="submit">Submit</button>
21     </form>
22   `
23 })
24 export class AppComponent {
25   user = {
26     username: '',
27     password: ''
28   };
29 
30   onSubmit(form: NgForm) {
31     if (form.valid) {
32       console.log(this.user);
33     }
34   }
35 }

Erklärung:

  • Zeile 7: Die lokale Variable “form” wird beim Aufruf der onSubmit-Methode übergeben
  • Zeile 30: Die onSubmit-Methode hat jetzt einen Parameter namens “form” vom Typ “NgForm”
  • Zeile 31: Wir nutzen form.valid, um zu überprüfen, ob das Formular gültig ist

Diskussion

Wie schon im Rezept “TDF: Ein einfaches Formular implementieren” erwähnt, erhält jedes form-Tag eine Instanz der NgForm-Direktive. Diese Instanz beinhaltet verschiedene Informationen über das Formular wie z. B. dessen Gültigkeitsstatus, dessen Controls und die Werte der Controls. Die Direktive hat eine exportAs-Eigenschaft mit dem Wert 'ngForm' (ein String). Den Wert der exportAs-Eigenschaft können wir im Template nutzen, um die Instanz der Direktive im Template zu referenzieren.

Ein Formular ist nur dann gültig, wenn alle seine Controls gültig sind. Die NgForm-Direktive überprüft also die einzelnen Controls und setzt die valid-Eigenschaft auf true, wenn jedes Control gültig ist.

Von Haus aus unterstützt Angular derzeit vier Validierungs-Attribute:

  • required,
  • pattern,
  • minlength und
  • maxlength.

Vermutlich wird es mit der Zeit noch mehr eingebaute Validierungs-Attribute bzw. Validierungs-Typen wie z. B. “email” und “url” geben. Siehe hierzu Github-Issues #2961 und #2962.

Code

Code auf Github für die erste Lösung

Live Demo der ersten Lösung auf angular2kochbuch.de

Code auf Github für die zweite Lösung

Live Demo der zweiten Lösung auf angular2kochbuch.de

Weitere Ressourcen

TDF: Fehlermeldungen für einzelne Formular-Felder anzeigen

Problem

Ich möchte für jedes ungültige Eingabefeld eine Fehlermeldung anzeigen. Je nachdem weshalb das Eingabefeld ungültig ist, soll die entsprechende Fehlermeldung angezeigt werden.

Zutaten

Lösung 1

In der ersten Lösung werden wir die Gültigkeit des jeweiligen Eingabefeldes überprüfen, indem wir über die lokale (Referenz-) Variable des Formulars auf das Control zugreifen.

app.component.ts
 1 import { Component } from '@angular/core';
 2 
 3 @Component({
 4   selector: 'app-root',
 5   template: `
 6     <form (ngSubmit)="onSubmit()" #form="ngForm" novalidate>
 7       <label>Username
 8         <input type="text"
 9           name="username"
10           [(ngModel)]="user.username"
11           required />
12       </label>
13       <div *ngIf="form.controls.username?.invalid">
14         This field is required!
15       </div>
16       <label>Password
17         <input type="password"
18           name="password"
19           [(ngModel)]="user.password"
20           required minlength="10" />
21       </label>
22       <div *ngIf="form.controls.password?.errors?.required">
23         This field is required!
24       </div>
25       <div *ngIf="form.controls.password?.errors?.minlength">
26         This field must have at least 10 characters
27       </div>
28       <button type="submit" [disabled]="form.invalid">Submit</button>
29     </form>
30   `
31 })
32 
33 ...

Erklärung:

  • Zeilen 13-15: Nutzung von ngIf mit Bedingung form.controls.username?.invalid. Damit greifen wir auf die invalid-Eigenschaft des Controls zu. Diese Eigenschaft ist true, wenn das Eingabefeld ungültig ist
  • Zeilen 22-24: Nutzung von ngIf mit Bedingung form.controls.password?.errors?.required. Die Bedingung ist wahr, wenn das Eingabefeld leer ist
  • Zeilen 25-27: Nutzung von ngIf mit Bedingung form.controls.password?.errors?.minlength. Die Bedingung ist wahr, wenn das Eingabefeld nicht leer ist und weniger als zehn Zeichen beinhaltet

Lösung 2

Hier werden wir lokale Variablen für jedes Eingabefeld (Control) definieren. Über die lokale Variable werden wir Zugriff auf die Gültigkeit des Eingabefelds bekommen.

app.component.ts
 1 import { Component } from '@angular/core';
 2 
 3 @Component({
 4   selector: 'app-root',
 5   template: `
 6     <form (ngSubmit)="onSubmit()" #form="ngForm" novalidate>
 7       <label>Username
 8         <input type="text"
 9           name="username"
10           [(ngModel)]="user.username"
11           required
12           #username="ngModel" />
13       </label>
14       <div *ngIf="username.invalid">
15         This field is required!
16       </div>
17       <label>Password
18         <input type="password"
19           name="password"
20           [(ngModel)]="user.password"
21           required minlength="10"
22           #password="ngModel" />
23       </label>
24       <div *ngIf="password?.errors?.required">
25         This field is required!
26       </div>
27       <div *ngIf="password.errors?.minlength">
28         This field must have at least 10 characters
29       </div>
30       <button type="submit" [disabled]="!form.valid">Submit</button>
31     </form>
32   `
33 })
34 
35 ...

Erklärung:

  • Zeile 12: Mit #username="ngModel" definieren wir eine lokale Variable namens “username”. Die Variable ist eine Referenz auf die Instanz der NgModel-Direktiven des Eingabefelds
  • Zeilen 14-16: Nutzung von ngIf mit Bedingung username?.invalid. Damit greifen wir auf die invalid-Eigenschaft des Controls zu. Diese Eigenschaft ist true, wenn das Eingabefeld ungültig ist
  • Zeile 22: Mit #password="ngModel" definieren wir eine lokale Variable namens “password”. Die Variable ist eine Referenz auf die Instanz der NgModel-Direktiven des Eingabefelds
  • Zeilen 24-26: Nutzung von ngIf mit Bedingung password.errors?.required. Die Bedingung ist wahr, wenn das Eingabefeld leer ist
  • Zeilen 27-29: Nutzung von ngIf mit Bedingung password.errors?.minlength. Die Bedingung ist wahr, wenn das Eingabefeld nicht leer ist und weniger als zehn Zeichen beinhaltet

Diskussion

Bei der Erklärung der Lösungen haben wir einige Details weggelassen. Nun möchten wir auch diese Details erklären. Wir fangen mit dem Elvis-Operator (?.) an.

Elvis-Operator

Den Elvis-Operator haben wir in beiden Lösungen verwendet. Dieser kann uns helfen, wenn wir im Template mit Objekten arbeiten, die null oder undefined sein könnten. Da das controls-Objekt des Formulars am Anfang leer ist (undefined), nutzen wir den Elvis-Operator, um Exceptions zu vermeiden. Wenn ein Eingabefeld gültig ist, ist das errors-Objekt des Controls null. Darum haben wir in beiden Lösungen den Elvis-Operator bei der Überprüfung der Gültigkeit des Passwort-Felds verwendet.

controls-Objekt

Das controls-Objekt der ngForm-Instanz beinhaltet alle Controls des Formulars. Wir können die einzelne Controls über ihren Namen (name-Attribut) referenzieren.

errors-Objekt

In beiden Lösungen haben wir das errors-Objekt benutzt, um Bedingungen für die NgIf-Direktiven zu definieren. Dieses beinhaltet die Gründe weshalb ein Eingabefeld ungültig ist. Wenn z. B. das required-Attribut eines Eingabefeldes definiert und das Feld leer ist, beinhaltet das errors-Objekt die Eigenschaft “required” mit dem Wert true. Der Name der Eigenschaft, in unserem Beispiel “required”, zeigt an, welche Validierung fehlschlägt. Dieser Fehlschlag ist der Grund für die Ungültigkeit des Eingabefeldes. Als zweite Bedingung für das Passwort-Feld haben wir “minlength” benutzt. Die Wahrheit ist, dass “minlength” keine boolesche Eigenschaft ist, sondern ein Objekt. Wenn das Eingabefeld genügend Zeichen beinhaltet, ist die minlength-Eigenschaft des errors-Objektes undefined. Wenn das Eingabefeld nicht genügend Zeichen beinhaltet, ist der Wert der Eigenschaft ein Objekt mit den Eigenschaften “actualLength” und “requiredLength”. Die erste Eigenschaft zeigt an, wie viele Zeichen im Eingabefeld enthalten sind. Die zweite Eigenschaft zeigt an, wie viele Zeichen wir mindestens brauchen bevor das Eingabefeld gültig wird. In der Lösung, die wir oben gezeigt haben, wäre der Wert für die requiredLength-Eigenschaft 10.

Lösung 2

Genau so wie die NgForm-Direktive hat auch die NgModel-Direktive eine exportAs-Eigenschaft mit dem Wert 'ngModel'. Somit können wir im Template Zugriff auf die Instanz der NgModel-Direktive und ihre Eigenschaften wie z. B. “valid” und “errors” erhalten.

Code

Code auf Github für die erste Lösung

Live Demo der ersten Lösung auf angular2kochbuch.de

Code auf Github für die zweite Lösung

Live Demo der zweiten Lösung auf angular2kochbuch.de

Weitere Ressourcen

TDF: Formular-Felder und CSS-Klassen

Problem

Ich möchte ein ungültiges Formular-Feld farblich hervorheben.

Zutaten

Lösung

Jedes Eingabefeld bekommt von Angular gewisse CSS-Klassen gesetzt. Um diese zu nutzen, müssen wir nur entsprechende Styles definieren.

app.component.ts
 1 import { Component } from '@angular/core';
 2 
 3 @Component({
 4   selector: 'app-root',
 5   styles: [
 6     '.ng-invalid { border-color: red; }',
 7     '.ng-valid { border-color: green; }'
 8   ],
 9   template: `
10     <form (ngSubmit)="onSubmit()" #form="ngForm" novalidate>
11       <label>Username
12         <input type="text"
13           name="username"
14           [(ngModel)]="user.username"
15           required />
16       </label>
17       <label>Password
18         <input type="password"
19           name="password"
20           [(ngModel)]="user.password"
21           required minlength="10" />
22       </label>
23       <button type="submit" [disabled]="form.invalid">Submit</button>
24     </form>`
25 })
26 
27 ...

Erklärung:

  • Zeilen 6-7: CSS-Styles für die “ng-invalid” und “ng-valid” CSS-Klassen

Diskussion

Beim Laden der Anwendung sieht Angular die Attribute “required” und “minlength” und setzt dann die CSS-Klasse “ng-invalid”, da die Eingabefelder leer und somit ungültig sind. Sobald wir mindestens einen Buchstaben in das Benutzername-Eingabefeld eingeben, wird das Eingabefeld gültig und Angular entfernt die ng-invalid-Klasse und setzt stattdessen die ng-valid-Klasse. Beim Eingabefeld für das Passwort wird die ng-valid-Klasse erst dann gesetzt, wenn wir mindestens zehn Zeichen eingeben.

Außer “ng-valid” und “ng-invalid” werden von Angular noch vier weitere CSS-Klassen gesetzt. Diese sind:

  • ng-touched/ng-untouched und
  • ng-dirty/ng-pristine.

Die ng-touched-Klasse wird gesetzt, wenn der Nutzer einmal in einem Eingabefeld drin war und danach raus gesprungen ist (focus dann blur). Beim Laden der Anwendung ist die ng-untouched-Klasse gesetzt. Die ng-dirty-Klasse wird gesetzt, sobald der Nutzer in ein Eingabefeld etwas geschrieben hat. Beim Laden der Anwendung ist die ng-pristine-Klasse gesetzt. Wir haben also drei CSS-Klassen Paare die Informationen über den Zustand eines Eingabefelds geben. Für das Formular (form-Tag) werden die gleiche CSS-Klassen gesetzt. Die ng-dirty-Klasse wird gesetzt sobald mindestens ein Eingabefeld die ng-dirty-Klasse bekommt. Die ng-touched-Klasse wird gesetzt sobald mindestens ein Eingabefeld die ng-touched-Klasse bekommt.

Code

Code auf Github

Live Demo auf angular2kochbuch.de

MDF: Formular mit dem FormBuilder implementieren

Problem

Ich möchte, dass sich die Logik für mein Formular in der Klasse der Komponente befindet. Somit ist mein Template nicht überladen und ich kann auch die Logik besser testen.

Zutaten

  • Angular 2 Anwendung
  • HTML für ein Formular
  • Das ReactiveForms-Modul von Angular
  • Der FormBuilder-Service von Angular
  • Die FormControlName-Direktive von Angular
  • Die FormGroupDirective-Direktive von Angular
  • Anpassungen an der app.module.ts-Datei
  • Anpassungen an der package.json-Datei

Lösung

Wir nutzen hier ein “Model-Driven” Formular. Wir definieren das Modell für das Formular und dessen Controls in der Klasse der Komponente. Im Template nutzen wir das form-Tag und mehrere input-Tags, die wir mit Hilfe der FormControlName- und der FormGroup-Direktiven mit dem Modell in der Klasse verbinden.

app.component.ts
 1 import { Component } from '@angular/core';
 2 import { FormBuilder, FormGroup } from '@angular/forms';
 3 
 4 @Component({
 5   selector: 'app-root',
 6   template: `
 7     <form (ngSubmit)="onSubmit()" [formGroup]="myForm" novalidate>
 8       <label>Username
 9         <input type="text" formControlName="username"/>
10       </label>
11       <label>Password
12         <input type="password" formControlName="password"/>
13       </label>
14       <button type="submit">Submit</button>
15     </form>
16   `
17 })
18 export class AppComponent {
19   myForm: FormGroup;
20   constructor(builder: FormBuilder) {
21     this.myForm = builder.group({
22       username: builder.control(''),
23       password: builder.control('')
24     });
25   }
26 
27   onSubmit() {
28     console.log(this.myForm.value);
29   }
30 }

Erklärung:

  • Zeilen 7-15: Das HTML für unser Formular
    • Zeile 7: Nutzung der FormGroupDirective-Direktiven (formGroup). Damit verbinden wir das Formular in der Klasse (Zeile 21) mit dem Formular im DOM. ngSubmit wird im Rezept “TDF: Ein einfaches Formular implementieren” erklärt
    • Zeile 9: Nutzung der FormControlName-Direktiven. Damit verbinden wir das username-Control in der Klasse (Zeile 22) mit dem Eingabefeld im DOM
    • Zeile 12: Nutzung der FormControlName-Direktiven. Damit verbinden wir das password-Control in der Klasse (Zeile 23) mit dem Eingabefeld im DOM
  • Zeile 20: Konstruktor der Klasse mit einer Instanz des FormBuilder-Services als Parameter
  • Zeilen 21-24: Das Modell für unser Formular
    • Zeile 21: Hier rufen wir die group-Methode auf, die eine Instanz der FormGroup-Klasse erzeugt
    • Zeile 22: Hier rufen wir die control-Methode auf, die eine Instanz der FormControl-Klasse erzeugt. Der default-Wert des Eingabefelds ist ein leerer String
    • Zeile 23: Gleiches wie oben, aber für das Passwort-Feld
  • Zeile 28: Hier greifen wir auf die Werte zu, die sich im Formular befinden

Da sich Formular-Direktiven wie z. B. “FormControlName” in einem eigenen Angular-Modul befinden, müssen wir dieses Modul in unser “AppModule” importieren

app.component.ts
 1 import { NgModule }      from '@angular/core';
 2 import { BrowserModule } from '@angular/platform-browser';
 3 import { ReactiveFormsModule } from '@angular/forms';
 4 
 5 import { AppComponent }  from './app.component';
 6 
 7 @NgModule({
 8   imports: [ BrowserModule, ReactiveFormsModule ],
 9   declarations: [ AppComponent ],
10   bootstrap: [ AppComponent ]
11 })
12 export class AppModule { }

Erklärung:

  • Zeile 8: Hier importieren wir das “ReactiveFormsModule” in unser Modul. In diesem Modul befinden sich alle Direktiven und Services, die wir für Model-Driven Forms brauchen

Da sich das “ReactiveFormsModule” in einem eigenen npm-Paket befindet, müssen wir dieses auch in der package.json deklarieren.

package.json
1 {
2   ...
3   "dependencies": {
4     ...
5     "@angular/forms": "2.1.2"
6     ...
7   }
8   ...
9 }

Wenn eine Angular-Anwendung mit angular-cli initialisiert wird, wird das “@angular/forms”-Paket von angular-cli in der package.json-Datei deklariert.

Diskussion

FormBuilder ist ein Service, den Angular uns zur Verfügung stellt. Dieser Service erlaubt es uns, mit relativ wenig Code komplexe Formulare zu definieren. Model-Driven Formulare können wir auch ohne den FormBuilder schreiben, indem wir selbst Instanze der FormGroup- bzw. FormControl-Klassen erzeugen und diese an das Template binden.

Wie schon erwähnt, arbeiten wir hier mit “Model-Driven Forms”. Das Modell für das Formular wird in der Klasse definiert und dann an das DOM gebunden. Mit [formGroup]="myForm" binden wir das Modell an das DOM-Formular an. Ohne diese Bindung würde Angular für das Formular eine neue Instanz der NgForm-Direktive erzeugen und wir hätten keine Verbindung zu unserem Modell. Da ein Formular ohne Eingabefelder nutzlos ist, haben wir beim Aufruf der group-Methoden ein Objekt mit zwei Controls übergeben. Die Eigenschaftsnamen im Objekt definieren die Namen der Controls (hier “username” und “password”) und die Werte sind Instanzen der FormControl-Klasse. Natürlich müssen wir unsere Controls auch an die eigentlichen Eingabefelder im DOM binden. Dies tun wir mit Hilfe der FormControlName-Direktiven.

Ein Detail hatten wir bis jetzt ignoriert. Wie wusste Angular überhaupt, dass wir den FormBuilder-Service brauchten? Anhand der Typinformation, die wir bei der Parameterdefinition im Konstruktor angegeben haben und mittels Dependency Injection (DI) kann Angular uns eine Instanz eines Services bei der Initialisierung der Komponente übergeben. Die Typinformation befindet sich auf Zeile 20 (app.component.ts) und ist der Teil nach dem Doppelpunkt. Wir haben builder: FormBuilder geschrieben. In diesem Fall ist “builder” der Parametername und “FormBuilder” die Typinformation. Mit Hilfe des Typs schaut Angular nach, welche Services zur Verfügung stehen und gibt uns eine Instanz mit dem richtigen Typ zurück. Dependency Injection in Angular ist ein relativ komplexes Thema und eine vollständige Erklärung würde den Rahmen eines Rezeptes sprengen. Wer mehr über dieses Thema lesen möchte, kann einige Informationen auf der Angular 2 Webseite finden.

Natürlich wollen wir auch Zugriff auf die Daten erhalten, die der Nutzer in das Formular eingegeben hat. Im Rezept “TDF: Ein einfaches Formular implementieren” haben wir dafür die NgModel-Direktive benutzt. Da wir hier die NgModel-Direktive nicht nutzen, brauchen wir einen anderen Weg, um auf die Daten zuzugreifen. Zum Glück beinhaltet auch unser Formular-Modell alle Daten, die in die jeweiligen Eingabefelder geschrieben worden sind. Die Daten befinden sich in der value-Eigenschaft des Formular-Modells. Diese Eigenschaft ist ein Objekt und beinhaltet alle Eingabefelder. Das value-Objekt hat als Eigenschaftsnamen die Namen der Controls, die wir im Formular-Modell definiert haben. Die Werte sind dann das was der Nutzer in den Eingabefeldern geschrieben hat. In unserem Beispiel besteht das value-Objekt aus zwei Eigenschaften mit den Namen “username” und “password”.

Template-Driven vs. Model-Driven Formulare

Unabhängig von der Formularart, können wir das selbe Ergebnis erzielen. Wenn wir dieses Rezept mit dem Rezept “Ein einfaches Formular implementieren” vergleichen, sehen wir, dass das Endresultat gleich ist. Beide Formulare besitzen zwei Eingabefelder und bei Submit greifen wir auf die Daten zu, die der Nutzer im Formular eingegeben hat. Der Unterschied zwischen den beiden Rezepten sind die Mittel, die wir benutzt haben, um das Problem zu lösen.

Für Template-Driven Formulare brauchen wir das “FormsModule” und für Model-Driven Formulare das “ReactiveFormsModule”. Direktiven und Klassen mit “Ng” bzw. “ng” als Präfix gehören zu den Template-Driven Formulare. Die Restlichen Direktiven, Klassen und Services gehören zu den Model-Driven Formularen oder können von beiden Formulararten benutzt werden.

Code

Code auf Github

Live Demo auf angular2kochbuch.de

Weitere Ressourcen

MDF: Gültigkeit eines Formulars überprüfen

Problem

Ich möchte ein Formular mit dem FormBuilder bauen und zusätzlich möchte ich auch in der Lage sein, zu erkennen, wann das Formular gültig ist.

In diesem Rezept lösen wir dasselbe Problem wie im Rezept “TDF: Gültigkeit eines Formulars überprüfen”. Nur werden wir hier mit Validierungsfunktionen anstelle von Validierungs-Attributen arbeiten.

Zutaten

Lösung 1

In dieser Lösung werden wir sehen, wie wir den Submit-Button deaktivieren können, wenn wir ein Model-Driven Formular nutzen. Diese Lösung ist äquivalent zur Lösung 1 im Rezept “TDF: Gültigkeit eines Formulars überprüfen”.

app.component.ts
 1 import { Component } from '@angular/core';
 2 import {
 3     FormBuilder,
 4     FormGroup,
 5     Validators
 6 } from '@angular/forms';
 7 
 8 @Component({
 9   selector: 'app-root',
10   template: `
11     <form (ngSubmit)="onSubmit()" [formGroup]="myForm" novalidate>
12       <label>Username
13         <input type="text" formControlName="username"/>
14       </label>
15       <label>Password
16         <input type="password" formControlName="password"/>
17       </label>
18       <button
19         type="submit"
20         [disabled]="myForm.invalid"
21       >Submit</button>
22     </form>
23   `
24 })
25 export class AppComponent {
26   myForm: FormGroup;
27 
28   constructor(builder: FormBuilder) {
29     this.myForm = builder.group({
30       username: builder.control('', Validators.required),
31       password: builder.control('', Validators.compose([
32         Validators.required,
33         Validators.minLength(10)
34       ]))
35     });
36   }
37 
38   onSubmit() {
39     console.log(this.myForm.value);
40   }
41 }

Erklärung:

  • Zeile 20: Hier binden wir die disabled-Eigenschaft an den Ausdruck myForm.invalid
  • Zeile 30: Control für das Benutzernamefeld definieren. Mit Validators.required definieren wir das Eingabefeld als Pflichtfeld
  • Zeile 31: Ein Control erwartet als zweiten Parameter eine Validierungsfunktion. Wenn wir mehrere Funktionen gleichzeitig nutzen möchten, müssen wir die compose-Funktion nutzen
  • Zeile 32: Hier definieren wir das Passwortfeld als Pflichtfeld
  • Zeile 33: Das Feld muss mindestens zehn Zeichen beinhalten, damit es gültig ist

Lösung 2

Diese Lösung ist äquivalent zur Lösung 2 im Rezept “TDF: Gültigkeit eines Formulars überprüfen”. Auch hier überprüfen wir die Gültigkeit des Formulars in der Komponenten-Klasse statt im Template. Das Template bleibt das gleiche wie im Rezept “MDF: Formular mit dem FormBuilder implementieren”.

app.component.ts
 1 ...
 2 
 3 export class AppComponent {
 4   myForm: FormGroup;
 5 
 6   constructor(builder: FormBuilder) {
 7     this.myForm = builder.group({
 8       username: builder.control('', Validators.required),
 9       password: builder.control('', Validators.compose([
10         Validators.required,
11         Validators.minLength(10)
12       ]))
13     });
14   }
15 
16   onSubmit() {
17     if (this.myForm.valid) {
18       console.log(this.myForm.value);
19     }
20   }
21 }

Erklärung:

  • Zeile 17: Hier überprüfen wir die Gültigkeit des Formulars, nach dem ein Submit-Event ausgelöst wurden ist durch z. B. ein Klick auf den Submit-Button

Code

Code auf Github für die erste Lösung

Live Demo der ersten Lösung auf angular2kochbuch.de

Code auf Github für die zweite Lösung

Live Demo der zweiten Lösung auf angular2kochbuch.de

Weitere Ressourcen

  • Offizielle Validators-Dokumentation auf der Angular 2 Webseite

MDF: Fehlermeldungen für einzelne Formular-Felder anzeigen

Problem

Ich möchte für jedes ungültige Eingabefeld eine Fehlermeldung anzeigen. Je nachdem weshalb das Eingabefeld ungültig ist, soll die entsprechende Fehlermeldung angezeigt werden.

Zutaten

Lösung

Wir werden die Gültigkeit des jeweiligen Eingabefeldes überprüfen, indem wir im Template über das Formular auf das jeweilige Control zugreifen.

app.component.ts
 1 import { Component } from '@angular/core';
 2 import {
 3   FormBuilder,
 4   FormGroup,
 5   Validators
 6 } from '@angular/forms';
 7 
 8 @Component({
 9   selector: 'app-root',
10   template: `
11     <form (ngSubmit)="onSubmit()" [formGroup]="myForm" novalidate>
12       <label>Username
13         <input type="text" formControlName="username"/>
14       </label>
15       <div *ngIf="myForm.controls.username.invalid">
16         This field is required!
17       </div>
18       <label>Password
19         <input type="password" formControlName="password"/>
20       </label>
21       <div *ngIf="myForm.controls.password.errors?.required">
22         This field is required!
23       </div>
24       <div *ngIf="myForm.controls.password.errors?.minlength">
25         This field must have at least 10 characters
26       </div>
27       <button type="submit" [disabled]="myForm.invalid">Submit</button>
28     </form>
29   `
30 })
31 export class AppComponent {
32   myForm: FormGroup;
33 
34   constructor(builder: FormBuilder) {
35     this.myForm = builder.group({
36       username: builder.control('', Validators.required),
37       password: builder.control('', Validators.compose([
38         Validators.required,
39         Validators.minLength(10)
40       ]))
41     });
42   }
43 
44   onSubmit() {
45     console.log(this.myForm.value);
46   }
47 }

Erklärung:

  • Zeilen 15-17: Nutzung von ngIf mit Bedingung myForm.controls.username.invalid. Damit greifen wir auf die invalid-Eigenschaft des Controls zu. Diese Eigenschaft ist true, wenn das Eingabefeld ungültig ist
  • Zeilen 21-23: Nutzung von ngIf mit Bedingung myForm.controls.password.errors?.required. Die Bedingung ist wahr, wenn das Eingabefeld leer ist
  • Zeilen 24-26: Nutzung von ngIf mit Bedingung myForm.controls.password.errors?.minlength. Die Bedingung ist wahr, wenn das Eingabefeld nicht leer ist und weniger als zehn Zeichen beinhaltet

Diskussion

Natürlich setzt Angular auch für Model-Driven Formulare entsprechende CSS-Klassen, die den Zustand eines Eingabefeldes kennzeichnen. Welche CSS-Klassen uns zur Verfügung stehen, steht im Diskussionsteil des Rezeptes “TDF: Formular-Felder und CSS-Klassen”.

Bei der Erklärung der Lösung haben wir einige Details weggelassen. Nun möchten wir auch diese Details erklären. Wir fangen mit dem Elvis-Operator (?.) an.

Elvis-Operator

Der Elvis-Operator kann uns helfen, wenn wir im Template mit Objekten arbeiten, die null oder undefined sein könnten. Wenn ein Eingabefeld gültig ist, ist das errors-Objekt des Controls null. Darum haben wir Elvis-Operator bei der Überprüfung der Gültigkeit des Passwort-Felds verwendet.

controls-Objekt

Das controls-Objekt der ngForm-Instanz beinhaltet alle Controls des Formulars. Wir können die einzelne Controls über ihren Namen (name-Attribut) referenzieren.

errors-Objekt

Das errors-Objekt beinhaltet die Gründe weshalb ein Eingabefeld ungültig ist. Wenn z. B. das required-Attribut eines Eingabefeldes definiert und das Feld leer ist, beinhaltet das errors-Objekt die Eigenschaft “required” mit dem Wert true. Der Name der Eigenschaft, in unserem Beispiel “required”, zeigt an, welche Validierung fehlschlägt. Dieser Fehlschlag ist der Grund für die Ungültigkeit des Eingabefeldes. Als zweite Bedingung für das Passwort-Feld haben wir “minlength” benutzt. Die Wahrheit ist, dass “minlength” keine boolesche Eigenschaft ist, sondern ein Objekt. Wenn das Eingabefeld genügend Zeichen beinhaltet, ist die minlength-Eigenschaft des errors-Objektes undefined. Wenn das Eingabefeld nicht genügend Zeichen beinhaltet, ist der Wert der Eigenschaft ein Objekt mit den Eigenschaften “actualLength” und “requiredLength”. Die erste Eigenschaft zeigt an, wie viele Zeichen im Eingabefeld enthalten sind. Die zweite Eigenschaft zeigt an, wie viele Zeichen wir mindestens brauchen bevor das Eingabefeld gültig wird. In der Lösung, die wir oben gezeigt haben, wäre der Wert für die requiredLength-Eigenschaft 10.

Code

Code auf Github

Live Demo auf angular2kochbuch.de

MDF: Eigene Validatoren definieren

Problem

Ich möchte überprüfen, ob ein Eingabefeld mindestens einen Großbuchstaben beinhaltet. Wenn kein Großbuchstabe vorhanden ist, soll das Eingabefeld ungültig sein.

Zutaten

Lösung

Wir werden hier die gleichen Validierungsfunktionen wie im Rezept “MDF: Formular mit dem FormBuilder und Validierung” nutzen. Wir werden zusätzlich eine eigene Validierungsfunktion namens “containsCapital” implementieren.

app.component.ts
 1 import { Component } from '@angular/core';
 2 import {
 3     FormBuilder,
 4     FormGroup,
 5     Validators,
 6     FormControl
 7 } from '@angular/forms';
 8 
 9 ...
10 
11 export class AppComponent {
12   myForm: FormGroup;
13 
14   constructor(builder: FormBuilder) {
15     this.myForm = builder.group({
16       username: builder.control('', Validators.required),
17       password: builder.control('', Validators.compose([
18         Validators.required,
19         Validators.minLength(10),
20         function containsCapital(control: FormControl) {
21           const reg = /[A-Z]/;
22           if (reg.test(control.value)) {
23             return null;
24           } else {
25             return {
26               containsCapital: true
27             };
28           }
29         }
30       ]))
31     });
32   }
33 
34   onSubmit() {
35     if (this.myForm.valid) {
36       console.log(this.myForm.value)
37     }
38   }
39 }

Erklärung:

  • Zeilen 20-29: Unsere Validierungsfunktion
    • Zeile 20: Als Parameter erhält die Validierungsfunktion eine Instanz der FormControl-Klasse. In diesem Fall ist die Instanz unser password-Control
    • Zeile 22: Überprüfung, ob der Wert (control.value) des Controls einen Großbuchstaben beinhaltet
    • Zeile 23: Wenn das Eingabefeld einen gültigen Wert besitzt, geben wir null zurück
    • Zeilen 25-27: Wenn das Eingabefeld einen ungültigen Wert besitzt, geben wir ein Objekt zurück

Diskussion

Wenn die Validierung fehlschlägt, muss die Validierungsfunktion ein nicht leeres Objekt zurückgeben. Wir erhalten auf dieses Objekt über das errors-Objekt des FormControls Zugriff. Dieses Objekt haben wir im Rezept “MDF: Fehlermeldungen für einzelne Formular-Felder anzeigen” gesehen. Solange das Passwort-Feld keinen Großbuchstaben enthält, hat das errors-Objekt eine Eigenschaft namens “containsCapital” mit Wert true. Wir hätten auch ein komplexeres Objekt zurückgeben können, genauso wie es die minLength-Validierungsfunktion tut. Wenn der Wert des Eingabefelds gültig ist, geben wir null zurück. Andere Werte wie z. B. undefined haben den gleichen Effekt. Da aber die Angular-Validierungsfunktionen auch null nutzen, um die Ungültigkeit zu kennzeichen, tun wir es hier auch.

Code

Code auf Github

Live Demo auf angular2kochbuch.de

MDF: Eigene asynchrone Validatoren definieren

Problem

Ich möchte überprüfen, ob der angegebene Benutzername bereits existiert. Dafür muss ich den Server kontaktieren und auf die Antwort warten, bevor ich weiß, ob das Eingabefeld gültig oder ungültig ist.

Zutaten

Lösung

Um die Lösung möglichst einfach zu halten, werden wir die Server-Anfrage mit einem Timeout simulieren. Für eine echte Server-Anfrage brauchen wir einen Server, der auf die Anfrage antworten kann und Code, der die Anfrage schicken kann.

app.component.ts
 1 import { Component } from '@angular/core';
 2 import {
 3     FormBuilder,
 4     FormGroup,
 5     Validators,
 6     FormControl
 7 } from '@angular/forms';
 8 
 9 ...
10 
11 export class AppComponent {
12   myForm: FormGroup;
13 
14   constructor(builder: FormBuilder) {
15     this.myForm = builder.group({
16       username: builder.control('', Validators.required,
17           function usernameExists(control: FormControl) {
18             return new Promise((resolve) => {
19               setTimeout(() => {
20                 if (control.value === 'Max') {
21                   resolve({
22                     usernameExists: true
23                   });
24                 } else {
25                   resolve(null);
26                 }
27               }, 1000);
28             });
29           }),
30       password: builder.control('', Validators.compose([
31         Validators.required,
32         Validators.minLength(10)
33       ]))
34     });
35   }
36 
37   onSubmit() {
38     if (!this.myForm.pending && this.myForm.valid) {
39       console.log(this.myForm.value);
40     }
41   }
42 }

Erklärung:

  • Zeilen 17-29: Unsere asynchrone Validierungsfunktion
    • Zeile 17: Als Parameter erhält die Validierungsfunktion eine Instanz der FormControl-Klasse. In diesem Fall ist die Instanz unser username-Control
    • Zeile 18: Asynchrone Validierungsfunktionen liefern Promises als Rückgabewert zurück
    • Zeile 19: Wir simulieren mit der setTimeout-Funktion eine Server-Anfrage
    • Zeile 20: Überprüfung, ob der Wert (control.value) des Controls gleich dem String Max ist
    • Zeilen 21-23: Wenn der Wert gleich Max ist, ist das Eingabefeld ungültig. Eir teilen Angular dies mit, indem wir der resolve-Funktion ein Objekt übergeben
    • Zeile 25: Wenn der Wert ungleich Max ist, ist das Eingabefeld gültig. Wir teilen Angular dies mit, indem wir der resolve-Funktion null übergeben
  • Zeile 38: Hier wird überprüft (this.myForm.pending), ob alle asynchronen Validierungsfunktionen eine Antwort erhalten haben

Diskussion

Eine asynchrone Validierungsfunktion ist sehr ähnlich zu einer synchronen Validierungsfunktion, wie wir sie im Rezept “MDF: Eigene Validatoren definieren” gesehen haben. Beide Funktionen erhalten eine Instanz der FormControl-Klasse als Funktionsparameter. Beide signalisieren die Gültigkeit des Eingabefelds, indem sie null und die Ungültigkeit des Eingabefelds, indem sie ein Objekt zurückgeben. Zwischen synchronen und asynchronen Validierungsfunktionen gibt es aber auch Unterschiede. Wir nutzen den dritten Parameter der control-Methode für asynchrone Validierungsfunktionen. Um mehrere asynchrone Validierungsfunktionen für ein Control zu definieren, müssen wir die composeAsync-Methode anstelle der compose-Methode nutzen. Asynchrone Validierungsfunktionen liefern ein Promise (oder ein Observable) als Rückgabewert zurück. Die Gültigkeit wird durch den Aufruf der resolve-Funktion angegeben.

Asynchrone Validierungsfunktionen besitzen noch weitere Besonderheiten. Sie werden nur dann aufgerufen, wenn das Eingabefeld nach dem Aufruf der synchronen Validierungsfunktionen gültig ist. Wenn es ungültig ist, werden die asynchronen Validierungsfunktionen nicht aufgerufen. Da wir auf die asynchronen Funktionen warten müssen, bevor wir die Gültigkeit des Eingabefelds und des Formulars prüfen können, wird von Angular die pending-Eigenschaft des FormControls und des Formulars auf true gesetzt, bis wir eine Antwort erhalten haben. Wir haben im Code bereits gesehen (Zeile 38), wie wir die pending-Eigenschaft nutzen können.

Es ist vermutlich bekannt, dass Promises zwei Funktionen besitzen: die resolve- und die reject-Funktion. Asynchrone Validierungsfunktionen benötigen die reject-Funktion nicht. Im Gegenteil, wenn wir “reject” nutzen, wird die pending-Eigenschaft true bleiben bis die resolve-Funktion aufgerufen wird. Es ist also wichtig, dass wir Fehler in der Validierungsfunktion abfangen und in der Fehlerbehandlungsroutine die resolve-Funktion aufrufen.

Code

Code auf Github

Live Demo auf angular2kochbuch.de

Weitere Ressourcen

  • Weitere Informationen über Promises auf der Mozilla Developer Network Webseite

MDF: Abhängige Eingabefelder validieren

Problem

Ich möchte sicherstellen, dass zwei voneinander abhängige Eingabefelder den gleichen Wert beinhalten. Z. B. ein Passwort und ein Passwort wiederholen Feld.

Zutaten

Lösung

Wie werden hier eine Validierungsfunktion definieren, die zwei Eingabefelder gleichzeitig validieren kann. Wir werden auch einen Fehler anzeigen, wenn nicht beide Eingabefelder den gleichen Wert haben.

app.component.ts
 1 import { Component } from '@angular/core';
 2 import {
 3     FormBuilder,
 4     FormGroup,
 5     Validators
 6 } from '@angular/forms';
 7 
 8 @Component({
 9   selector: 'app-root',
10   template: `
11     <form (ngSubmit)="onSubmit()" [formGroup]="myForm" novalidate>
12       <label>Username
13         <input type="text" formControlName="username"/>
14       </label>
15       <label formGroupName="passwords">Password
16         <input type="password" formControlName="password"/>
17       </label>
18       <label formGroupName="passwords">Repeat Password
19         <input type="password" formControlName="passwordRepeat"/>
20       </label>
21       <div *ngIf="myForm.controls.passwords.hasError('passwordsNotEqual')">
22         Passwords are not equal
23       </div>
24       <button type="submit">Submit</button>
25     </form>
26   `
27 })
28 export class AppComponent {
29   myForm: FormGroup;
30 
31   constructor(builder: FormBuilder) {
32     this.myForm = builder.group({
33       username: builder.control('', Validators.required),
34       passwords: builder.group({
35         password: builder.control('', Validators.compose([
36           Validators.required,
37           Validators.minLength(10)
38         ])),
39         passwordRepeat: builder.control('')
40       }, {
41         validator(group: FormGroup) {
42           if (group.value.password !== group.value.passwordRepeat) {
43             return {
44               passwordsNotEqual: true
45             };
46           }
47           return null;
48         }
49       })
50     });
51   }
52 
53   onSubmit() {
54     if (this.myForm.valid) {
55       console.log(this.myForm.value);
56     }
57   }
58 }

Erklärung:

  • Zeilen 15 und 18: Hier sagen wir Angular, dass die zwei Password-Eingabefelder zu der “FormGroup” mit Name “passwords” gehören
  • Zeilen 21-23: Mit myForm.controls.passwords.hasError(‘passwordsNotEqual’) fragen wir die FormGroup, ob sie einen Fehler namens “passwordsNotEqual” hat. Wenn die zwei Eingabefelder der FormGroup nicht den gleichen Wert haben, wird die Bedingung true sein
  • Zeilen 34-49: Definition einer “FormGroup” namens “passwords”
    • Zeilen 35-39: Die Controls der FormGroup
    • Zeilen 41-48: Unsere Validierungsfunktion

Diskussion

Ein Formular in Angular ist eine “FormGroup” und kann nebst Controls auch weiter FormGroups beinhalten, die wiederum Controls und weitere FormGroups beinhalten können. In unserem Beispiel haben wir eine FormGroup namens “passwords” definiert, die zwei Controls hat. Das password- und das passwordRepeat-Control. Diese FormGroup ist nur dann gültig (valid-Eigenschaft is true), wenn die jeweilige Controls gültig sind und, wenn die Validierungsfunktionen der Gruppe null zurückliefern. Darum nutzen wir myForm.controls.passwords.hasError(‘passwordsNotEqual’) und nicht einfach myForm.controls.passwords.invalid als Bedingung für die NgIf-Direktive. Alternativ hätten wir auch das errors-Objekt (myForm.controls.passwords.errors?.passwordsNotEqual) nutzen können, wie wir es in anderen Rezepten auch getan haben.

Der zweite Parameter der group-Methode (Zeilen 40-49) bekommt ein Objekt mit zwei optionale Eigenschaft. Die eine Eigenschaft hat den Namen “validator”, diese ist die Eigenschaft, die wir auch hier nutzen. Die andere Eigenschaft hat den Namen “asyncValidator” und wird benutzt, wenn wir für eine FormGroup asynchrone Validierungsfunktionen definieren möchten. Wichtig ist, dass beide Eigenschaften eine Funktion als Wert haben. Falls wir mehrere synchrone bzw. asynchrone Validierungsfunktionen brauchen, müssen wir die compose- bzw. die composeAsync-Methode nutzen wie im Rezept “MDF: Eigene Validatoren definieren” gezeigt wird. Da wird nur das Beispiel mit der compose-Methode gezeigt. Die composeAsync-Methode funktioniert analog.

In den meisten MDF-Rezepten dieses Buches, war das value-Objekt des Formulars flach. Es hatte eine Eigenschaft für jedes Control. Auch in diesem Rezept hat das value-Objekt eine Eigenschaft für jedes Control allerdings sind die Werte für “password” und “passwordRepeat” verschachtelt. Diese befinden sich in einem Objekt namens “passwords”. Das heißt, dass das value-Objekt die Struktur des Formulars hat, wie wir diese über den FormBuilder definiert haben. Je nachdem was der Server von uns erwartet, müssen wir Werte, die sich in Untergruppen befinden herausziehen und diese auf der höchste Ebene definieren. Das value-Objekt sieht in diesem Rezept so aus:

1 {
2   username: "Wert im Eingabefeld"
3   passwords: {
4     password: "Wert im Eingabefeld",
5     passwordRepeat: "Wert im Eingabefeld"
6   }
7 }

Code

Code auf Github

Live Demo auf angular2kochbuch.de

Weitere Ressourcen

  • Offizielle FormGroupName-Dokumentation auf der Angular 2 Webseite

Rezepte für den Datenaustausch

In der Regel möchten wir bei Web-Anwendungen in der Lage sein, mit einem Server Daten auszutauschen. Wir möchten zur Laufzeit sowohl Daten nachladen als auch Daten, die uns der Nutzer gibt, an den Server schicken. In diesem Kapitel werden wir den Http-Service von Angular kennenlernen und sehen, wie wir ihn nutzen können, um mit einem Server zu kommunizieren.

Daten vom Server mit GET holen

Problem

Ich möchte zur Laufzeit Daten im JSON-Format von einem Server holen.

Zutaten

  • Ein Service, um die Daten zu holen
  • Das Http-Modul von Angular
  • Der Http-Service von Angular
  • Die map-Methode für Observables (Ist Teil von RxJS)
  • Auf Nutzer-Input reagieren (Wir holen Daten nach einem Klick auf einen Button)
  • Liste von Daten anzeigen
  • Anpassungen an der app.module.ts-Datei
  • Anpassungen an der package.json-Datei

Lösung

In dieser Lösung werden wir sehen, wie wir JSON-Daten von einem Server holen können. Die Fehlerbehandlung lassen wir außen vor, damit wir uns fürs Erste auf die GET-Anfrage konzentrieren können. Über Fehler bei Server-Anfragen reden wir im Rezept “Server-Anfragen und Fehlerbehandlung”.

Wir gehen hier davon aus, dass wir einen Server haben, der auf 127.0.0.1:3000 hört. Wenn eine GET-Anfrage an /data geschickt wird, antwortet der Server mit Status 200 und Daten im JSON-Format. Wir nutzen http://127.0.0.1:3000/data als URL für die Anfrage. Die Daten sehen wie folgt aus:

Server-Antwort
1 {
2   "data": [{"id": 1, "name": "a"}, {"id": 2, "name": "b"}]
3 }

Jetzt implementieren wir einen Service, um die GET-Anfrage zu schicken.

data.service.ts
 1 import { Injectable } from '@angular/core';
 2 import { Http } from '@angular/http';
 3 import 'rxjs/add/operator/map';
 4 
 5 @Injectable()
 6 export class DataService {
 7   http: Http;
 8   url: string;
 9   constructor(http: Http) {
10     this.http = http;
11     this.url = 'http://127.0.0.1:3000/data';
12   }
13 
14   getData() {
15     const observable = this.http.get(this.url);
16     const anotherObservable = observable.map((response) => response.json().data);
17     return anotherObservable;
18   }
19 }

Erklärung:

  • Zeile 2: Hier importieren wir den Http-Service von Angular
  • Zeile 3: Durch diesen Import erweitern wir die Instanzen der Observable-Klasse (siehe Zeile 16) um eine Methode namens “map”
  • Zeile 5: Im Gegensatz zum Service im Rezept “Ein Service definieren” ist der Injectable-Decorator hier nicht optional, da unser Service eine Abhängigkeit besitzt: den Http-Service (siehe Zeile 9)
  • Zeile 9: Hier definieren wir den Http-Service als Abhängigkeit unseres Services
  • Zeile 15: Hier rufen wir die get-Methode des Http-Services auf. Als Parameter erhält diese Methode eine URL. Der Rückgabewert ist ein Observable (Teil von RxJS)
  • Zeile 16: Wir nutzen die map-Methode, um die Antwort des Servers zu transformieren. Der response-Parameter ist eine Instanz der Response-Klasse und besitzt eine json-Methode (.json()), die die Daten des Servers in ein Objekt transformiert

Jetzt müssen wir noch unsere Komponente aus “Ein Service definieren” anpassen, so dass diese mit dem Http-Service und Observables arbeiten kann. Wir werden die Daten nach einem Klick auf den “Get Data”-Button holen und diese in einer Liste anzeigen.

app.component.ts
 1 import { Component } from '@angular/core';
 2 
 3 import { DataService } from './data.service';
 4 
 5 interface Data {
 6   id: number;
 7   name: string;
 8 }
 9 
10 @Component({
11   selector: 'app-root',
12   template: `
13     <button (click)="getData()">Get Data</button>
14     <ul>
15       <li *ngFor="let d of data">ID: {{d.id}} Name: {{d.name}}</li>
16     </ul>
17   `
18 })
19 export class AppComponent {
20   dataService: DataService;
21   data: Array<Data>;
22 
23   constructor(dataService: DataService) {
24     this.dataService = dataService;
25     this.data = [];
26   }
27 
28   getData() {
29     this.dataService.getData()
30         .subscribe((data) => {
31           this.data = data;
32         });
33   }
34 }

Erklärung:

  • Zeile 21: Die data-Eigenschaft wird benutzt, um die Daten in der Liste anzuzeigen. Sie ist vom Typ “Data” (Siehe Zeilen 5-8)
  • Zeilen 28-33: Methode, die aufgerufen wird, wenn der Nutzer auf den “Get Data”-Button klickt
    • Zeile 30: Die getData-Methode des Services gibt ein Observable zurück. Jedes Observable hat eine subscribe-Methode die wir nutzen können, um auf Änderungen zu reagieren, indem wir der Methode eine Callback-Funktion übergeben

Da sich der Http-Service in einem eigenen Angular-Modul befindet, müssen wir dieses Modul in unser “AppModule” importieren.

app.module.ts
 1 import { NgModule } from '@angular/core';
 2 import { BrowserModule } from '@angular/platform-browser';
 3 import { HttpModule } from '@angular/http';
 4 
 5 import { AppComponent } from './app.component';
 6 import { DataService } from './data.service';
 7 
 8 @NgModule({
 9   imports: [ BrowserModule, HttpModule ],
10   declarations: [ AppComponent ],
11   bootstrap: [ AppComponent ]
12   providers: [ DataService ]
13 })
14 export class AppModule { }

Erklärung:

  • Zeile 8: Hier importieren wir das “HttpModule” in unser Modul. Jetzt können wir alle Services, die dieses Modul definiert in unserem Code nutzen

Da sich das “HttpModule” in einem eigenen npm-Paket befindet, müssen wir dieses auch in der package.json-Datei deklarieren.

package.json
1 {
2   ...
3   "dependencies": {
4     ...
5     "@angular/http": "2.1.2"
6     ...
7   }
8   ...
9 }

Wenn eine Angular-Anwendung mit angular-cli initialisiert wird, wird das “HttpModule” automatisch von angular-cli importiert und das entsprechende npm-Paket in der package.json-Datei deklariert.

Diskussion

Wir hätten den Http-Service auch direkt in unserer Komponenten nutzen können. Wir haben uns stattdessen für die Nutzung eines weiteren Services entschieden. Der Grund dafür ist, dass wir die Logik für den Aufruf nicht in unserer Komponenten haben wollen. Die Komponente interessiert sich nicht dafür, wie wir die Daten bekommen. Diese benötigt nur ein Array mit Daten. Woher dieses Array stammt, ist der Komponente egal. Mit dem gesonderten Service ist es einfacher z. B. die URL zu ändern, ohne dass wir die Komponente anpassen müssen. Wir passen nur den Service an und alle Komponenten, die diesen Service benutzen, werden weiterhin funktionieren. Es ist allgemein ein “Best Practice” unsere Komponenten schlank zu halten und Logik wie z. B. “Wie hole ich Daten?” einem Service zu überlassen.

Code

Code auf Github

Code für den Server: server.js. Der Server funktioniert mit Node.js.

Weitere Ressourcen

Daten mit POST an den Server schicken

Problem

Ich möchte mittels POST-Anfrage Daten an einen Server schicken.

Zutaten

Lösung

Wir werden hier die Lösung aus dem Rezept “Daten vom Server mit GET holen” erweitern, so dass wir auch Daten an den Server schicken können. Auch hier lassen wir die Fehlerbehandlung außen vor. Siehe dazu “Server-Anfragen und Fehlerbehandlung”.

Wir gehen davon aus, dass wir einen Server haben, der auf 127.0.0.1:3000 hört. Wenn eine POST-Anfrage an /data geschickt wird, antwortet der Server mit Status 200 und Daten im JSON-Format. Bei der Anfrage erwartet der Server ein Objekt im JSON-Format mit einer name-Eigenschaft. Für die Antwort wird dieses Objekt um eine id-Eigenschaft erweitert. Wir nutzten http://127.0.0.1:3000/data als URL für die Anfrage.

Daten für die Anfrage
1 {
2   "name": "New Name"
3 }
Dazugehörige Antwort
1 {
2   "data": {
3     "id": 3,
4     "name": "New Name"
5   }
6 }

Unser DataService wird um eine neue Methode und einen Import erweitert. Der Rest bleibt zum Rezept “Daten vom Server mit GET holen” gleich.

data.service.ts
 1 import { Injectable } from '@angular/core';
 2 import { Http } from '@angular/http';
 3 import 'rxjs/add/operator/map';
 4 
 5 @Injectable()
 6 export class DataService {
 7 
 8   constructor(http: Http) {...}
 9 
10   getData() {...}
11 
12   sendData(name) {
13     const data = { name: name };
14 
15     const observable = this.http.post(this.url, data);
16     const anotherObservable = observable.map((response) => response.json().data);
17     return anotherObservable;
18   }
19 }

Erklärung:

  • Zeilen 12-18: Methode, die wir aufrufen, um Daten an den Server zu schicken
    • Zeile 13: Die Daten, die wir zum Server schicken wollen
    • Zeile 15: Aufruf der post-Methode mit den Daten als zweitem Parameter

Unsere Komponente wird auch um eine sendData-Methode erweitert.

app.component.ts
 1 ...
 2 
 3 @Component({
 4   selector: 'app-root',
 5   template: `
 6     <button (click)="getData()">Get Data</button>
 7     <button (click)="sendData()">Send Data</button>
 8     <ul>
 9       <li *ngFor="let d of data">ID: {{d.id}} Name: {{d.name}}</li>
10     </ul>
11   `
12 })
13 export class AppComponent {
14 
15   ...
16 
17   constructor(dataService: DataService) {...}
18 
19   getData() {...}
20 
21   sendData() {
22     const name = 'New Name';
23     this.dataService.sendData(name)
24         .subscribe((data) => {
25           this.data.push(data);
26         });
27   }
28 }

Erklärung:

  • Zeile 7: Neuer Button. Bei Klick wird die sendData-Methode aufgerufen
  • Zeilen 21-27: Methode, die aufgerufen wird, um Daten zu schicken
    • Zeile 22: Die Daten, die wir schicken wollen
    • Zeile 23: Aufruf der sendData-Methode des DataService. Diese Methode gibt ein Observable zurück.
    • Zeile 24: Die Callback-Funktion (Observer) der subscribe-Methode wird aufgerufen, wenn der Server uns eine Antwort geschickt hat. Die data-Variable ist ein Objekt mit den Eigenschaften “name” und “id”
    • Zeile 25: Das neue Objekt wird der Liste mit den Daten hinzugefügt

Diskussion

Wir haben bis jetzt ein Detail über die Observables, die die Methoden der Http-Klasse zurückgeben, verschwiegen. Die subscribe-Methode ist nicht nur eine Möglichkeit, mittels einer Callback-Funktion auf Änderungen zu reagieren. Ohne den Aufruf der subscribe-Methode (mit oder ohne Callback) würde Angular gar keine Server-Anfrage schicken. Der Grund dafür ist, dass die Http-Methoden sogenannte “Cold Observables” zurückgeben. Cold Observables führen erst dann die gewünschte Operation, wenn jemand die subscribe-Methode des Observable aufruft. In unserer Lösung ist die Operation die POST-Anfrage und unser “jemand” die sendData-Methode der Komponente. Siehe auch Cold vs. Hot Observables.

Code

Code auf Github

Code für den Server: server.js. Der Server funktioniert mit Node.js.

Server-Anfragen und Fehlerbehandlung

Problem

Ich möchte dem Nutzer eine sinnvolle Fehlermeldung anzeigen, wenn bei einer Server-Anfrage etwas schief läuft.

Zutaten

Lösung

Wir werden den Code aus dem Rezept “Daten vom Server mit GET holen” anpassen. Fehler bei POST- und weiteren Server-Anfragen können wir analog behandeln.

Wir gehen hier davon aus, dass wir einen Server haben der auf 127.0.0.1:3000 hört. Wenn eine Anfrage nach /error geschickt wird, antwortet der Server mit Status 500 und Daten im JSON-Format. Wir nutzen http://127.0.0.1:3000/error als URL für die Anfrage.

Server-Antwort
1 {
2   "error": "Invalid Url"
3 }
data.service.ts
 1 ...
 2 
 3 import 'rxjs/add/operator/catch';
 4 import { Observable } from 'rxjs/Observable';
 5 import 'rxjs/add/observable/throw';
 6 
 7 @Injectable()
 8 export class DataService {
 9   ...
10 
11   constructor(http: Http) {
12     this.http = http;
13     this.url = 'http://127.0.0.1:3000/data';
14   }
15 
16   getData() {
17     const observable = this.http.get(this.url)
18         .map((response) => response.json().data);
19     const anotherObservable = observable.catch((response) => {
20       return this.handleResponseError(response);
21     });
22     return anotherObservable;
23   }
24 
25   handleResponseError(response) {
26     let errorString = '';
27     if (response.status === 500) {
28       errorString = `Server error: ${response.json().error}`;
29     } else {
30       errorString = 'Some error occurred';
31     }
32     return Observable.throw(errorString);
33   }
34 }

Erklärung:

  • Zeile 3: Durch diesen Import erweitern wir die Instanzen der Observable-Klasse (siehe Zeile 18) um eine Methode namens “catch”
  • Zeile 4: Hier importieren wir die Observable-Klasse von RxJS
  • Zeile 5: Durch diesen Import erweitern wir die Observable-Klasse (siehe Zeile 31) um eine statische Methode namens “throw”
  • Zeile 13: Die URL, um einen Server-Fehler zu erzwingen
  • Zeilen 19-21: Hier wird die catch-Methode benutzt, um Fehler beim Server-Aufruf zu behandeln
    • Zeile 20: Wenn ein Fehler auftritt, wird die handleResponseError-Methode aufgerufen
  • Zeilen 25-33: Methode, um Server-Fehler zu behandeln
    • Zeile 32: Wir geben eine Observable-Instanz mit Fehler zurück, so dass der zweite Parameter der subscribe-Methode aufgerufen wird (siehe Zeile 24 im Ausschnitt aus der app.component.ts-Datei)

Den eigentlichen Server-Fehler haben wir schon im Service mit Hilfe der catch-Methode behandelt. Da wir zusätzlich dem Nutzer eine sinnvolle Fehlermeldung anzeigen möchten, müssen wir den Fehler auch in der Komponente behandeln.

app.component.ts
 1 ...
 2 
 3 @Component({
 4   selector: 'app-root',
 5   template: `
 6     <button (click)="getData()">Get Data</button>
 7     <p>
 8       Error: {{errorText}}
 9     </p>
10     <ul>
11       <li *ngFor="let d of data">ID: {{d.id}} Name: {{d.name}}</li>
12     </ul>
13   `
14 })
15 export class AppComponent {
16   ...
17 
18   errorText: string;
19 
20   constructor(dataService: DataService) {... }
21 
22   getData() {
23     this.dataService.getData()
24         .subscribe((data) => {
25           this.data = data;
26         }, (errorText) => {
27           this.errorText = errorText;
28         });
29   }
30 }

Erklärung:

  • Zeile 8: Fehlermeldung in der View anzeigen
  • Zeilen 26-28: Fehlerbehandlungsfunktion als zweiter Parameter der subscribe-Methode
    • Zeile 27: Fehlertext von Zeile 31 des Services als Wert der errorText-Eigenschaft setzen

Diskussion

Die catch-Methode von Instanzen der Observable-Klasse ist vergleichbar mit der catch-Methode einer Promise-Kette oder dem catch-Block einer try/catch-Anweisung. Jeder Fehler in der Kette der Observables, der vor der catch-Methode auftritt, kann in der catch-Methode behandelt werden. Ähnlich wie bei einem try/catch und bei Promises wird bei einen Fehler die Ausführungs-Kette “unterbrochen” und wir springen direkt zu der catch-Methode. Bei der Fehlerbehandlung in der catch-Methode können wir eine Instanz eines Observable mit Fehler zurückgeben, um erneut einen Fehler zu erzeugen. Das ist ähnlich zu einer throw-Anweisung in einem catch-Block einer try/catch-Anweisung.

Wie wir in diesem Rezept gesehen haben, kann die subscribe-Methode nicht nur eine Callback-Funktion als Parameter erhalten, sondern zwei (genauer gesagt sind es drei aber der dritte Parameter ist für uns vorerst nicht relevant). Wir wissen schon, dass die erste Callback-Funktion aufgerufen wird, wenn die Server-Anfrage erfolgreich ist. Die zweite Callback-Funktion wird im Falle eines Fehlers aufgerufen. Diese Callback-Funktion ist unsere zweite Möglichkeit, einen Fehler in einer Observables-Kette zu behandeln. Wir haben also den Fehler an zwei verschiedenen Orten behandelt: Einmal in unserem Service mittels catch-Methode und einmal in unserer Komponente mit Hilfe des zweiten Parameters der subscribe-Methode. Prinzipiell wäre es möglich, den Fehler entweder im Service oder in der Komponente zu behandeln. Der zweite Parameter der subscribe-Methode ist optional und optional ist auch die Nutzung der catch-Methode. Der Grund, weshalb wir den Fehler an zwei Stellen behandeln, ist ganz einfach. Wir wollen nicht, dass unsere Komponente wissen muss, wie die Server-Antwort im Falle eines Fehlers aussieht, genauso, wie wir nicht wollten, dass die Komponente weiß, was mit einer erfolgreichen Server-Antwort zu tun ist, bevor Daten angezeigt werden können. Der Komponente reicht es, Daten bzw. Fehlermeldungen zu erhalten, die direkt angezeigt werden können.

Code

Code auf Github

Code für den Server: server.js. Der Server funktioniert mit Node.js.

Server-Anfrage mit Query-Parametern

Problem

Ich möchte bei der Anfrage Query-Parameter an den Server schicken.

Zutaten

Lösung

Wir konzentrieren uns in der Lösung auf GET-Anfragen, da diese am Häufigsten mit Query-Parametern benutzen werden. Wir können aber auch z. B. bei POST-Anfragen Query-Parameter mitschicken.

data.service.ts
 1 import { Injectable } from '@angular/core';
 2 import {
 3     Http,
 4     RequestOptions,
 5     URLSearchParams
 6 } from '@angular/http';
 7 import 'rxjs/add/operator/map';
 8 
 9 @Injectable()
10 export class DataService {
11 
12   ...
13 
14   getData() {
15     const limit = 1;
16 
17     const params = new URLSearchParams();
18     params.set('limit', String(limit));
19 
20     const requestOptions = new RequestOptions({search: params});
21 
22     const observable = this.http.get(this.url, requestOptions);
23     const anotherObservable = observable.map((response) => response.json().data);
24     return anotherObservable;
25   }
26 }

Erklärung:

  • Zeile 17: Erzeugen einer Instanz der URLSearchParams-Klasse
  • Zeile 18: Query-Parameter “limit” mit Wert '1' (der zweite Parameter der set-Methode muss ein String sein) definieren
  • Zeile 20: Erzeugen einer Instanz der RequestOptions-Klasse. Wir setzen unsere “params” als Wert der search-Eigenschaft. Die search-Eigenschaft definiert die Query-Parameter der Anfrage
  • Zeile 22: Aufruf der get-Methode mit einer URL und Optionen für die Anfrage

Diskussion

Wir können die Query-Parameter auch mittels String-Konkatenierung definieren, indem wir selbst einen Query-String zusammensetzen und diesen mit der URL konkatenieren. Für ein bis zwei Parameter können wir dies auch tun, aber für viele Parameter ist diese Lösung nicht wirklich geeignet. Die Nutzung der URLSearchParams-Klasse hat in diesem Fall zwei Vorteile. Zum Einen wird der Code lesbarer, wenn wir pro Parameter eine Zeile Code haben. Zum Anderen kümmert sich Angular um das richtige Format für den String, der später als Teil der URL mitgeschickt wird.

Code

Code auf Github

Code für den Server: server.js

Weitere Ressourcen

Server-Anfrage abbrechen (cancel)

Problem

Ich möchte dem Nutzer die Möglichkeit anbieten, eine Server-Anfrage abzubrechen, wenn diese zu lange dauert.

Zutaten

  • Daten vom Server mit GET holen
  • Neue URL, um eine Anfrage zu simulieren, die drei Sekunden braucht
  • Änderungen an der Komponente aus “Daten vom Server mit GET holen”
  • Die Subscription-Klasse von RxJS (wird nur für die Typdefinition gebraucht)

Lösung

In unserem Service (data.service.ts) haben wir nur eine Änderung durchgeführt. Und zwar haben wir die url-Eigenschaft angepasst. Diese besitzt jetzt den Wert 'http://127.0.0.1:3000/longrequest'.

app.component.ts
 1 ...
 2 
 3 import { Subscription } from 'rxjs/Subscription';
 4 
 5 ...
 6 
 7 @Component({
 8   selector: 'app-root',
 9   template: `
10     <button (click)="getData()">Get Data</button>
11     <button (click)="cancelRequest()">Cancel</button>
12     <ul>
13       <li *ngFor="let d of data">ID: {{d.id}} Name: {{d.name}}</li>
14     </ul>
15   `
16 })
17 export class AppComponent {
18   ...
19 
20   subscription: Subscription;
21 
22   constructor(dataService: DataService) {...}
23 
24   getData() {
25     this.subscription = this.dataService.getData()
26         .subscribe((data) => {
27           this.data = data;
28         });
29   }
30 
31   cancelRequest() {
32     if (this.subscription) {
33       this.subscription.unsubscribe();
34     }
35   }
36 }

Erklärung:

  • Zeile 3: Hier importieren wir die Subscription-Klasse von RxJS, die wir in Zeile 17 als Typ nutzen
  • Zeile 25: Hier speichern wir den Rückgabewert der subscribe-Methode in die subscription-Eigenschaft der Komponenteninstanz
  • Zeilen 31-35: Methode, die aufgerufen wird, wenn der Nutzer den cancel-Button klickt

Diskussion

Die subscribe-Methode der Instanzen der Observables-Klasse gibt eine Instanz der Subscription-Klasse zurück. Wir können auf dieser Instanz die unsubscribe-Methode aufrufen, um die dazugehörigen Observables wegzuwerfen. Nach dem Aufruf der unsubscribe-Methode werden die Observables gelöscht und deren Callback-Funktionen nicht mehr aufgerufen. Auch z. B. die Callback-Funktion der map-Methode (diese wird in der data.service.ts benutzt) wird nicht mehr aufgerufen. Also erzeugen die Observables keine neuen Werte mehr und der Fluss (stream) wird unterbrochen. Bei Server-Anfragen wird auch die abort-Methode der XMLHttpRequest-Instanz aufgerufen und die Anfrage wird abgebrochen.

Code

Code auf Github

Code für den Server: server.js. Der Server funktioniert mit Node.js.

Weitere Ressourcen

  • Die RxJS-Dokumentation bietet weitere Informationen über Subscriptions

Rezepte für Routing

In der Regel brauchen, vor allem größere Anwendungen verschiedene Views, die abhängig von eine Kondition angezeigt werden. Eine Möglichkeit so etwas zu implementieren, ist mittels Client-Seitigen Routing. Je nach Browser-URL, wird die Anwendung dem Nutzer eine andere View bzw. andere Inhalte anzeigen. Da wir beim Client-Seitigen Routing die Browser-URL als einer Art Zustand nutzen, kann ein Nutzer z. B. die URL in ein anderes Browser-Fenster kopieren und einfach dort weiter machen wo er war.

In diesem Kapitel, werden wir uns mit dem Router, den Angular uns zur Verfügung stellt beschäftigen. Wir werden uns nicht alle mögliche Funktionen des Routers anschauen. Dafür bräuchte man ein eigenes Buch. Ziel des Kapitels ist es mit ein paar wenige Rezepte, eine Basis zu schaffen, die man später je nach konkreten Anwendungsfall erweitern kann.

Einfaches Routing implementieren

Problem

Ich möchte meine Anwendung mit Hilfe des Routers in drei Teilbereichen aufspalten.

Zutaten

  • Angular 2 Anwendung
  • Eine Komponente für jeden Teilbereich
  • Das Router-Modul von Angular
  • Router-Konfiguration mit Pfaden für die drei Teilbereiche (app.routes.ts)
  • Anpassungen an der app.component.ts- und der app.module.ts-Datei
  • Die RouterLink-Direktive von Angular-Router
  • Die RouterOutlet-Direktive von Angular-Router
  • Anpassungen an der package.json-Datei

Lösung

Wie schon erwähnt, wollen wir die Anwendung in drei Teilbereichen aufspalten. Diese sind “Home”, “Products” und “Admin” und für jeden diese Teilbereiche werden wir eine Komponente implementieren.

home.component.ts
1 import { Component } from '@angular/core';
2 
3 @Component({
4   template: '<h1>Home</h1>'
5 })
6 export class HomeComponent {}

Erklärung:

Wie wir sehen, haben wir jetzt keine selector-Eigenschaft für den @Component-Decorator definiert. Wir brauchen diese nicht, weil unsere Komponente nicht in einem Template referenziert wird, sondern vom Router abhängig vom Pfad geladen wird. Die Restlichen zwei Komponenten zeigen wir hier nicht, diese werden analog zu der “HomeComponent” definiert.

Damit der Router weiß welcher Pfad bzw. URL zu welcher Komponente gehört, müssen wir diesen jetzt konfigurieren. Wir tun dies in eine eigene Datei, so dass es später einfacher ist die Konfiguration zu finden und zu erweitern bzw. anpassen.

app.routes.ts
 1 import { RouterModule, Routes } from '@angular/router';
 2 
 3 import { AdminComponent } from './admin.component';
 4 import { HomeComponent } from './home.component';
 5 import { ProductsComponent } from './products.component';
 6 
 7 const routes: Routes = [
 8   { path: '', component: HomeComponent },
 9   { path: 'admin', component: AdminComponent },
10   { path: 'products', component: ProductsComponent },
11 ];
12 
13 export const routing = RouterModule.forRoot(routes);

Erklärung:

  • Zeile 1: Als Erstes importieren wir die nötigen Abhängigkeiten aus dem @angular/router-Paket. Dieses kann mittels npm installiert werden
  • Zeilen 7-11: Unsere Router-Konfiguration. Die path-Eigenschaft ist der Teil nach der Domain der URL. Z. B. in der URL “http://localhost:4200/foo” ist “foo” der Pfad
    • Zeile 8: Wenn kein Pfad angegeben wird, wird die “HomeComponent” angezeigt
    • Zeile 9: Wenn der Pfad “admin” ist, wird die “AdminComponent” angezeigt
    • Zeile 10: Wenn der Pfad “products” ist, wird die “ProductsComponent” angezeigt
  • Zeile 13: Hier wird die Konfiguration dem Router übergeben. Die forRoot-Methode gibt dann ein Angular-Modul zurück mit der Konfiguration und Services, die uns die Arbeit mit dem Router erleichtern

Wie jedes andere Angular-Modul, müssen wir auch das Routing-Modul in unser “AppModule” importieren.

app.module.ts
 1 import { NgModule }      from '@angular/core';
 2 import { BrowserModule } from '@angular/platform-browser';
 3 
 4 import { routing } from './app.routes';
 5 import { AppComponent }  from './app.component';
 6 import { AdminComponent } from './admin.component';
 7 import { HomeComponent } from './home.component';
 8 import { ProductsComponent } from './products.component';
 9 
10 @NgModule({
11   imports: [ BrowserModule, routing ],
12   declarations: [
13     AppComponent, AdminComponent,
14     HomeComponent, ProductsComponent
15   ],
16   bootstrap: [ AppComponent ]
17 })
18 export class AppModule { }

Bis jetzt haben wir die Komponenten für unsere Teilbereiche definiert und den Router konfiguriert. Jetzt müssen wir die Teilbereiche noch anzeigen und ein einfaches Menü für die Navigation definiert. Wir tun dies in der app.component.ts-Datei.

app.component.ts
 1 import { Component } from '@angular/core';
 2 
 3 @Component({
 4   selector: 'app-root',
 5   template: `
 6     <nav>
 7       <ul>
 8         <li><a routerLink="">Home</a></li>
 9         <li><a routerLink="products">Products</a></li>
10         <li><a routerLink="admin">Admin</a></li>
11       </ul>
12     </nav>
13     <router-outlet></router-outlet>
14   `
15 })
16 export class AppComponent {}

Erklärung:

  • Zeilen 6-12: Die Navigation für unsere Anwendung. Mit der RouterLink-Direktive definieren wir, zu welchem Teilbereich der Router hin navigieren soll, wenn der Nutzer auf das Element klickt. Die Pfade, die wir der RouterLink-Direktive übergeben, müssen zu den path-Eigenschaften in der Router-Konfiguration passen
  • Zeile 13: Mit der RouterOutlet-Direktive, sagen wir dem Router wo er die Teilbereiche anzeigen soll. Je nach Pfad bzw. Routerzustand, entscheidet der Router welcher Teilbereich angezeigt werden muss

Da sich das “RouterModule” in einem eigenen npm-Paket befindet, müssen wir dieses auch in der package.json deklarieren.

package.json
1 {
2   ...
3   "dependencies": {
4     ...
5     "@angular/router": "3.1.2"
6     ...
7   }
8   ...
9 }

Wenn eine Angular-Anwendung mit angular-cli initialisiert wird, wird das “RouterModule” automatisch von angular-cli importiert und das entsprechende npm-Paket in der package.json-Datei deklariert.

Diskussion

Standardmäßig nutzt der Angular-Router HTML5-URLs. Allerdings brauchen die meisten Webserver eine spezielle Konfiguration, damit diese mit HTML5-URLs umgehen können. Aus diesem Grund bietet uns Angular auch die Möglichkeit Hash-Basierte (#) URLs zu nutzen. Wie das geht wird im Rezept “Hash-Basiert URLs für das Routing” gezeigt

Code

Code auf Github

Weitere Ressourcen

Hash-Basierte URLs für das Routing

Problem

Ich möchte Hash-Basierte URLs für das Routing nutzen, da mein Webserver mit HTML5-URLs nicht umgehen kann.

Zutaten

  • Einfaches Routing implementieren
  • Den LocationStrategy-Service von Angular-Common
  • Den HashLocationStrategy-Service von Angular-Common
  • Anpassungen an der app.module.ts-Datei

Lösung

app.module.ts
 1 import { NgModule } from '@angular/core';
 2 import { BrowserModule } from '@angular/platform-browser';
 3 import {
 4   LocationStrategy,
 5   HashLocationStrategy
 6 } from '@angular/common';
 7 
 8 import { routing } from './app.routes';
 9 import { AppComponent }  from './app.component';
10 import { AdminComponent } from './admin.component';
11 import { HomeComponent } from './home.component';
12 import { ProductsComponent } from './products.component';
13 
14 @NgModule({
15   imports: [ BrowserModule, routing ],
16   declarations: [
17     AppComponent, AdminComponent,
18     HomeComponent, ProductsComponent
19   ],
20   providers: [ { provide: LocationStrategy, useClass: HashLocationStrategy } ],
21   bootstrap: [ AppComponent ]
22 })
23 export class AppModule { }

Erklärung:

  • Zeile 4: Hier importieren wir den LocationStrategy-Service. Dieser sagt Angular wie die URLs auszusehen haben (mit Hash oder HTML5)
  • Zeile 5: Hier importieren wir den HashLocationStrategy-Service. Dieser wird für Hash-Basierte URLs benutzt
  • Zeile 20: Hier sagen wir Angular, dass zur Laufzeit der LocationStrategy-Service, den HashLocationStrategy-Service als Implementierung nutzen soll

Diskussion

Standardmäßig wird eine Instanz der PathLocationStrategy-Klasse (HTML5-URLs) zurückgegeben, wenn mittels Dependency Injection eine Instanz des LocationStrategy-Services gefragt ist. “LocationStrategy” ist ein sogenanntes “Token” und es definiert den Namen eines Services. Die useClass-Eigenschaft definiert welche Klasse für das Token benutzt werden soll. Hier (Zeile 20) sagen wir dem Injector: “Wenn jemand das Token “LocationStrategy” nutzt, um einen Service als Abhängigkeit zu definieren, sollst du eine Instanz der Klasse “HashLocationStrategy” zurückgeben”. Im Rezept Einen Service definieren, haben wir für das providers-Array nur “DataService” angegeben. Die Schreibweise, die wir dort benutzt haben ist äquivalent zu { provide: DataService, useClass: DataService }. Wenn also das Token und die Klasse den gleichen Namen haben, brauchen wir nicht die Objekt-Schreibweise, wie wir diese hier benutzt haben.

Eine vollständige Erklärung, wie Dependency Injection funktioniert und was wir alles damit machen können, gibt es auf der Angular 2 Webseite: Dependency Injection und Hierarchical Dependency Injectors.

Code

Code auf Github

Live Demo auf angular2kochbuch.de

Weitere Ressourcen

Die aktuelle Route hervorheben

Problem

Ich möchte die aktuelle Route in der Navigation farblich hervorheben, damit der Nutzer weiß wo er/sie sich aktuell befindet.

Zutaten

Lösung

Der einfachste Weg, die aktuelle Route farblich hervorzuheben ist mit Hilfe einer CSS-Klasse und der RouterLinkActive-Direktive. Dafür brauchen wir nur die app.component.ts-Datei anzupassen.

app.component.ts
 1 import { Component } from '@angular/core';
 2 
 3 @Component({
 4   selector: 'app-root',
 5   styles: ['.active-route { color: red; }'],
 6   template: `
 7     <nav>
 8       <ul>
 9         <li><a routerLink=""
10           routerLinkActive="active-route"
11           [routerLinkActiveOptions]="{exact: true}">Home</a></li>
12         <li><a routerLink="products"
13           routerLinkActive="active-route">Products</a></li>
14         <li><a routerLink="admin"
15           routerLinkActive="active-route">Admin</a></li>
16       </ul>
17     </nav>
18     <router-outlet></router-outlet>
19   `
20 })
21 export class AppComponent {}

Erklärung:

  • Zeile 5: Hier definieren wir die “active-route”-CSS-Klasse. Siehe auch Das Template der Komponente von dem CSS trennen
  • Zeilen 10, 13 und 15: Mittels der RouterLinkActive-Direktive, setzen wir die “active-route”-CSS-Klasse auf das Element, das auf die aktuelle Route zeigt

Diskussion

Die RouterLinkActive-Direktive nimmt den Wert der RouterLink-Direktive und ermittelt, ob der Pfad der RouterLink-Direktive der gleiche ist wie der Pfad der aktuellen Route. Falls ja wird der Wert nach dem Gleichheitszeichen als CSS-Klasse für das Element gesetzt. In unserem Fall wird die “active-route”-CSS-Klasse auf das a-Tag gesetzt. Wenn z. B. der Pfad “admin” ist, wird die CSS-Klasse auf das a-Tag mit routerLink=”admin” gesetzt. Für die “Home”-Route nutzen wir einen leeren String als Pfad. Da der leerer String Teil jedes Pfades ist, wird der a-Tag für die “Home”-Route immer als aktuell/aktiv gesetzt. Um das zu verhindern, sagen wir dem Router mittels [routerLinkActiveOptions]=”{exact: true}”, dass das a-Tag mit routerLink=”“ nur dann die aktuelle Route ist, wenn der Pfad der aktuellen Route ein leerer String ist.

Code

Code auf Github

Live Demo auf angular2kochbuch.de. Nutzt Hash-Basierte URLs: “Hash-Basierte URLs für das Routing

Weitere Ressourcen

Umleitung für unbekannte Pfade

Problem

Ich möchte, dass unbekannte Pfade zu der Hauptkomponente umgeleitet (redirect) werden.

Zutaten

Lösung

app.routes.ts
 1 import { RouterModule, Routes } from '@angular/router';
 2 
 3 import { AdminComponent } from './admin.component';
 4 import { HomeComponent } from './home.component';
 5 import { ProductsComponent } from './products.component';
 6 
 7 const routes: Routes = [
 8   { path: '', component: HomeComponent },
 9   { path: 'admin', component: AdminComponent },
10   { path: 'products', component: ProductsComponent },
11   { path: '**', redirectTo: '' },
12 ];
13 
14 export const routing = RouterModule.forRoot(routes);

Erklärung:

  • Zeile 11: Hier definieren wir eine Route mit Pfad “**”, die immer dann greift, wenn keine andere Route zu der Browser-URL passt. Die redirectTo-Eigenschaft bekommt als Wert den Pfad zu den wir umleiten möchten

Code

Code auf Github

Live Demo auf angular2kochbuch.de. Nutzt Hash-Basierte URLs: “Hash-Basierte URLs für das Routing

Navigation in der Klasse der Komponente

Problem

Ich möchte zu eine andere Komponente navigieren, indem ich auf einen Button klicke.

Zutaten

  • Einfaches Routing implementieren
  • Den ActivatedRoute-Service vom Angular-Router
  • Den Router-Service vom Angular-Router
  • Anpassungen an der app.component.ts-Datei

Lösung

Wir wollen jetzt nicht mit Hilfe der RouterLink-Direktive navigieren, sondern mit der navigate-Methode des Routers. Diese können wir nutzen, um zu navigieren als Reaktion z. B. auf ein Event nach dem Speichern von Daten.

app.component.ts
 1 import { Component } from '@angular/core';
 2 import { ActivatedRoute, Router } from '@angular/router';
 3 
 4 @Component({
 5   selector: 'app-root',
 6   template: `
 7     <nav>
 8       <ul>
 9         <li>
10           <button type="button" (click)="navigate('')">
11             Home
12           </button>
13         </li>
14         <li>
15           <button type="button" (click)="navigate('products')">
16             Products
17           </button>
18         </li>
19         <li>
20           <button type="button" (click)="navigate('admin')">
21             Admin
22           </button>
23         </li>
24       </ul>
25     </nav>
26     <router-outlet></router-outlet>
27   `
28 })
29 export class AppComponent {
30   route: ActivatedRoute;
31   router: Router;
32   constructor(route: ActivatedRoute, router: Router) {
33     this.route = route;
34     this.router = router;
35   }
36 
37   navigate(path) {
38     this.router.navigate([ path ], { relativeTo: this.route });
39   }
40 }

Erklärung:

Die a-Tags mit der RouterLink-Direktive, die wir im Rezept “Einfaches Routing implementieren” benutzt haben, haben wir hier mit einem button-Tag ersetzt. Jeder Button ruft bei einem Klick die navigate-Methode der Komponenten-Klasse und übergibt den Pfad.

  • Zeile 32: Hier injizieren wir den ActivatedRoute- und den Router-Service. Der ActivatedRoute-Service repräsentiert die aktuelle Route
  • Zeilen 37-39: navigate-Methode, die von den Buttons aufgerufen wird
    • Zeile 38: Hier wird die navigate-Methode des Routers aufgerufen. Dieser Aufruf ist äquivalent zu der RouterLink-Direktive, die wir schon gesehen haben

Diskussion

Wenn wir mit einem Pfad der ohne Slash (/) beginnt navigieren z. B. “admin”, führen wir eine relative Navigation aus. Dem entsprächen heißen Pfade, die ohne Slash beginnen “relative Pfade”. Auch Pfade, die mit “./” (die Pfade “admin” und “./admin” sind äquivalent) oder “../” beginnen sind relativ. Pfade, die mit einem Slash beginnen z. B. “/admin” heißen “absolute Pfade”. Immer wenn wir relative Pfade nutzen, müssen wir dem Router sagen relativ zu welcher Route wir navigieren möchten. Im Falle der RouterLink-Direktive wird immer relativ zu der aktuellen Route navigiert. Mit der navigate-Methode müssen wir explizit die Route angeben zu der wir relativ Navigieren wollen, indem wir die relativeTo-Eigenschaft setzen.

Die Auswirkungen von relativen Pfaden sind vor allem sichtbar, wenn wir innerhalb einer Komponente navigieren, die einen eigenen nicht leeren Pfad hat z. B. AdminComponent mit “admin” als Pfad. In unserem Beispiel hätten wir problemlos auch absolute Pfade nutzen können z. B. in Zeile 15 “/products” statt “products”. Wenn wir absolute Pfade nutzen, brauchen wir den zweiten Parameter ({relativeTo: this.route}) der navigate-Methode nicht.

Code

Code auf Github

Live Demo auf angular2kochbuch.de. Nutzt Hash-Basierte URLs: “Hash-Basierte URLs für das Routing

Weitere Ressourcen

Routing-Parameter

Problem

Ich möchte eine Komponente implementieren, die unterschiedliche Inhalte in der View anzeigt abhängig von einem Parameter in der URL.

Zutaten

  • Einfaches Routing implementieren
  • Anpassungen an der products.component.ts-Datei
  • Eine Komponente. Der Inhalt der View der Komponente wird sich abhängig von der Route-Parameter ändern. Diese Komponente (ProductComponent) zeigt das jeweilige Produkt an
  • ActivatedRoute-Service von Angular-Router
  • Anpassungen an der app.routes.ts-Datei

Lösung

Als Erstes definieren wir eine parametrisierte Route. Diese bekommt die id-Eigenschaft eines Produktes als Parameter und die “ProductComponent” als Komponente.

app.routes.ts
 1 import { RouterModule, Routes } from '@angular/router';
 2 
 3 import { AdminComponent } from './admin.component';
 4 import { HomeComponent } from './home.component';
 5 import { ProductsComponent } from './products.component';
 6 import { ProductComponent } from './product.component';
 7 
 8 const routes: Routes = [
 9   { path: '', component: HomeComponent },
10   { path: 'admin', component: AdminComponent },
11   { path: 'products', component: ProductsComponent },
12   { path: 'products/:id', component: ProductComponent },
13 ];
14 
15 export const routing = RouterModule.forRoot(routes);

Erklärung:

  • Zeile 12: Unsere neue Route. Mit dem Doppelpunkt und einen Namen (hier id) definieren wir einen Route-Parameter

Jetzt wollen wir eine Liste von Produkten für “ProductsComponent” definieren. Die id-Eigenschaft eines Produktes wird als Route-Parameter benutzt :id wird also zur Laufzeit durch die ID des Produktes ersetzt.

products.component.ts
 1 import { Component } from '@angular/core';
 2 
 3 @Component({
 4   template: `
 5     <h1>Products</h1>
 6     <ul>
 7       <li *ngFor="let prod of products">
 8         <a [routerLink]="prod.id">{{prod.name}}</a>
 9       </li>
10     </ul>
11   `
12 })
13 export class ProductsComponent {
14   products = [{
15     id: 1,
16     name: 'Product 1'
17   }, {
18     id: 2,
19     name: 'Product 2'
20   }, {
21     id: 3,
22     name: 'Product 3'
23   }];
24 }

Erklärung:

  • Zeile 8: Hier nutzen wir die RouterLink-Direktive und definieren damit zu welchem Produkt wir hin navigieren möchten. Wir nutzen eine Eigenschaft-Bindung (eckige Klammern) weil der Pfad, im Gegensatz zu den Pfaden in der app.component.ts-Datei, nicht konstant ist

Als letztes definieren wir die “ProductComponent”. Diese wird die id-Eigenschaft aus der Route lesen und sie in der View anzeigen.

product.component.ts
 1 import { Component } from '@angular/core';
 2 import { ActivatedRoute } from '@angular/router';
 3 
 4 @Component({
 5   template: `<h1>Product {{id}}</h1>`
 6 })
 7 export class ProductComponent {
 8   id: string;
 9 
10   constructor(route: ActivatedRoute) {
11     this.id = route.snapshot.params['id'];
12   }
13 }

Erklärung:

  • Zeile 11: Hier lesen wir den id-Parameter aus der aktuellen Route. Der String, hier ‘id’ muss der gleiche sein wie der String nach dem Doppelpunkt in der Pfaddefinition (app.routes.ts-Datei). Alternativ können wir die id-Eigenschaft in der ngOnInit-Methode lesen. Mehr Informationen über diese Methode gibt es im Rezept “Code ausführen bei der Initialisierung einer Komponente

Diskussion

In der app.routes.ts-Datei, haben wir den Pfad “products/:id” definiert aber in der “ProductsComponent” haben wir der RouterLink-Direktive (Zeile 8) nur die ID (prod.id) übergeben und trotzdem funktioniert das Routing. Wir wollen jetzt kurz verstehen warum das so ist.

Wir nutzen hier einen relativen Pfad (den Wert von prod.id) und die RouterLink-Direktive befindet sich in einer Komponente die einen nicht leeren Pfad (“products”) hat. In diesem Fall werden die zwei Pfade konkateniert. Das heißt, wenn wir zur Laufzeit auf den Link für das erste Produkt klicken, navigiert der Router zu der Komponente mit Pfad “products/1” (das erste Produkt hat die ID 1). Ausführlichere Informationen zu relativen bzw. absoluten Pfaden gibt es in der Diskussion des Rezepts “Navigation in der Klasse der Komponente

Code

Code auf Github

Live Demo auf angular2kochbuch.de. Nutzt Hash-Basierte URLs: “Hash-Basierte URLs für das Routing

Weitere Ressourcen

Rezepte für Komponenten

In diesem Kapitel befinden sich verschiedene Rezepte, die hauptsächlich Komponenten betreffen. Manche der Rezepte können auch bei Direktiven angewendet werden. Sachen, wie z. B. die Kommunikation zwischen Komponenten, werden in diesem Kapitel behandelt.

Komponente und HTML-Template trennen

Problem

Ich hab ein langes Angular-Template und ich möchte das HTML getrennt von meiner Komponente halten.

Zutaten

Lösung

app.component.ts
1 ...
2 
3 @Component({
4   selector: 'app-root',
5   templateUrl: './app.component.html'
6 })
7 
8 ...

Erklärung:

  • Zeile 5: Statt der template-Eigenschaft, die wir in anderen Rezepten benutzt haben, nutzen wir jetzt die templateUrl-Eigenschaft. Der angegebene Pfad ist relativ zu der app.component.ts-Datei

Diskussion

Wichtig zu beachten ist, dass wir nur entweder die template-Eigenschaft oder die templateUrl-Eigenschaft verwenden können. Beide gleichzeitig gehen nicht. Da wir in diesem Buch angular-cli mit Webpack nutzen wird die Zeile 5 oben beim Kompilieren durch template: require('./app.component.html') ersetzt und Angular wird sich zur Laufzeit so verhalten als ob wir selbst das Template in der Komponente geschrieben haben. Falls andere Build-Tools benutzt werden, die die templateUrl-Eigenschaft nicht ersetzen, wird zur Laufzeit die Datei von Angular mittels XMLHttpRequest vom Server geholt und der Inhalt der Datei kompiliert und in das DOM gesetzt. Im allgemeinen ist es Toolabhängig, ob wir einen relativen Pfaden angeben können und wie dieser genau aussieht. Wer mehr über relative Pfade für Templates erfahren möchte, kann den Artikel Component-Relative Paths lesen.

templateUrl- vs. template-Eigenschaft

Beide Ansätze haben Vor- und Nachteile. Wir werden uns diese kurz anschauen.

templateUrl-Eigenschaft

Vorteile Nachteile
Übersichtlicher Extra Server-Aufruf
  (Gilt in unserem Fall nicht)
Logik und Markup sind Zwei offene Dateien
getrennt um eine Komponente zu implementieren
Code-Highlighting Wie genau der Pfad aussieht ist
  Toolabhängig
Auto-Vervollständigung  

template-Eigenschaft

Vorteile Nachteile
Die gesamte Komponente wird Unübersichtlich, wenn wir viel
in einer Datei definiert HTML haben
Kein extra Server-Aufruf Codehighlighting und
  Autovervollständigung sind
  editorabhängig
Keine Probleme mit Pfaden  

Ich persönlich versuche immer kleine Komponenten mit wenig HTML (10-15 Zeilen) zu schreiben und nutze dabei die template-Eigenschaft.

Code

Code auf Github

Live Demo auf angular2kochbuch.de

Das Template der Komponente vom CSS trennen

Problem

Ich möchte meine CSS-Styles getrennt von meinem Template und nicht in einem style-Tag im Template halten.

Zutaten

Lösung

Statt die CSS-Klassen im Template zu halten, können wir die styles-Eigenschaft der Komponente nutzen.

app.component.ts
 1 import { Component } from '@angular/core';
 2 
 3 @Component({
 4   selector: 'app-root',
 5   styles: [
 6     '.box {width: 100px; height: 100px; background-color: red; margin: 10px}',
 7     '.box-blue {background-color: blue;}'
 8   ],
 9   template: `
10     <div class="box"></div>
11     <div class="box"></div>
12     <div class="box box-blue"></div>
13     <div class="box"></div>
14   `
15 })
16 export class AppComponent {}

Erklärung:

  • Zeilen 5-8: CSS-Styles für unsere Komponente

Diskussion

Die styles-Eigenschaft einer Komponente erwartet ein Array von Strings. Ein String kann CSS-Styles für eine oder mehrere Klassen, Tags, etc. beinhalten. Jeder String wird dann zur Laufzeit als style-Tag in den DOM gesetzt. In unserem Beispiel werden zwei style-Tags im Head des Dokuments hinzugefügt.

Wenn wir in Komponenten CSS-Styles definieren, können die definierten CSS-Styles standardmäßig nur in der Komponente verwendet werden, in der diese definiert worden sind. Es ist dabei egal, ob wir die CSS-Styles als inline-styles mittels style-Tag, über die styles-Eigenschaft oder über die styleUrls-Eigenschaft der Komponente definieren. Dieses Verhalten kann uns vor Fehlern schützen und meidet Konflikte in den CSS-Styles, wenn wir z. B. Komponenten wiederverwenden. Die Kapselung von Styles und Komponenten wird in Angular “View-Encapsulation” genannt.

Code

Code auf Github

Live Demo auf angular2kochbuch.de

Weitere Ressourcen

Komponente und CSS trennen

Problem

Ich hab viele CSS-Klassen und ich möchte diese nicht in der Komponente halten, sondern in einer separaten CSS-Datei.

Zutaten

Lösung

In der ersten Lösung hatten wir den Pfad relativ zur index.html-Datei angegeben. Es gibt auch die Möglichkeit, den Pfad relativ zur app.component.ts-Datei zu definieren.

app.component.ts
 1 ...
 2 
 3 @Component({
 4   selector: 'app-root',
 5   styleUrls: ['./app.component.ts'],
 6   template: `
 7     <div class="box"></div>
 8     <div class="box"></div>
 9     <div class="box box-blue"></div>
10     <div class="box"></div>
11   `
12 })
13 
14 ...

Erklärung:

  • Zeile 5: Statt der styles-Eigenschaft, die wir in anderen Rezepten benutzt haben, nutzen wir jetzt die styleUrls-Eigenschaft. Der angegebene Pfad ist relativ zu der app.component.ts-Datei

Diskussion

Die styleUrls-Eigenschaft einer Komponente erwartet ein Array von Strings. Da wir in diesem Buch angular-cli mit Webpack nutzen wird die Zeile 5 oben beim Kompilieren durch styles: [require('./app.component.css')] ersetzt und Angular wird sich zur Laufzeit so verhalten als ob wir selbst die Styles in der Komponente geschrieben haben. Falls andere Build-Tools benutzt werden, die die styleUrls-Eigenschaft nicht ersetzen, wird zur Laufzeit die Datei von Angular mittels XMLHttpRequest vom Server geholt und der Inhalt der Datei wird als style-Tag in das DOM gesetzt. Im allgemeinen ist es Toolabhängig, ob wir einen relativen Pfaden angeben können und wie dieser genau aussieht. Wer mehr über relative Pfade für Styles erfahren möchte, kann den Artikel Component-Relative Paths lesen.

Wenn wir in Komponenten CSS-Styles definieren, können die definierten CSS-Styles standardmäßig nur in der Komponente verwendet werden, in der diese definiert worden sind. Es ist dabei egal, ob wir die CSS-Styles als inline-styles mittels style-Tag, über die styles-Eigenschaft der Komponente oder über die styleUrls-Eigenschaft der Komponente definieren. Dieses Verhalten kann uns vor Fehlern schützen und meidet Konflikte in den CSS-Styles, wenn wir z. B. Komponenten wiederverwenden. Die Kapselung von Styles und Komponenten wird in Angular “View-Encapsulation” genannt.

Die Diskussion styles- vs. stuleUrls-Eigenschaft ist analog zur template- vs. templateUrl-Eigenschaft Diskussion in Komponente und HTML-Template trennen.

Code

Code auf Github

Live Demo auf angular2kochbuch.de

Weitere Ressourcen

Daten an eine Unterkomponente mittels input-Eigenschaft übergeben

Problem

Ich möchte Daten, die sich in der Überkomponente befinden, an eine Unterkomponente übergeben.

Zutaten

Lösung

Wir werden uns als Erstes die Überkomponente (Parent) und als Zweites die Unterkomponente (Child) anschauen.

app.component.ts
 1 import { Component } from '@angular/core';
 2 
 3 @Component({
 4   selector: 'app-root',
 5   template: `
 6     <p>Parent Data: {{parentData}}</p>
 7     <app-second [childData]="parentData"></app-second>
 8   `
 9 })
10 export class AppComponent {
11   parentData: string = 'Hello World!';
12 }

Erklärung:

  • Zeile 7: Hier nutzen wir eine Eigenschaft-Bindung, um den Wert der parentData-Eigenschaft an die childData-Eigenschaft der Unterkomponente zu übergeben
second.component.ts
1 import { Component, Input } from '@angular/core';
2 
3 @Component({
4   selector: 'app-second',
5   template: '<p>Child Data: {{childData}}</p>'
6 })
7 export class SecondComponent {
8   @Input() childData: string;
9 }

Erklärung:

  • Zeile 8: Mit Hilfe des Input-Decorators (@Input) definieren wir die childData-Eigenschaft als input-Eigenschaft. Zu beachten ist, dass die input-Eigenschaft den gleichen Namen wie der Name zwischen den eckigen Klammern in der app.component.ts Zeile 8 haben muss

Diskussion

Änderungen in der parentData-Eigenschaft werden zur Laufzeit in die childData-Eigenschaft propagiert. Wir müssen also nichts tun, wenn sich z. B. durch Nutzer-Interaktion der Wert der parentData-Eigenschaft ändert. Wenn wir einen neuen Wert für die childData-Eigenschaft setzen, wird dieser Wert in der Parent-Component nicht sichtbar sein. Wir haben also hier eine Einweg-Datenbindung zwischen Parent- und Child-Component. Allerdings müssen wir aufpassen, wenn wir mit Objekten arbeiten. Falls die parentData-Eigenschaft ein Objekt ist und wir eine Eigenschaft dieses Objekts in der Child-Component ändern, ist die Änderung auch in der Parent-Component sichtbar. Der Grund dafür ist, dass Angular keine Kopie des Objekts erstellt, sondern die Referenz weitergibt. Wir haben für dieses Rezept zwei Beispielanwendungen auf Github. Der Code im Solution-Verzeichnis ist dieser, den wir hier gezeigt haben. Das Demo-Verzeichnis beinhaltet eine Anwendung, die demonstrieren soll, welche Datenänderungen wo sichtbar sind.

Code

Code auf Github für die Lösung

Code auf Github für die Demonstration

Live Demo (Code aus dem Demo-Verzeichnis) auf angular2kochbuch.de.

Daten an die Überkomponente mittels output-Eigenschaft übergeben

Problem

Ich möchte Daten, die sich in einer Unterkomponente befinden, an die Überkomponente übergeben.

Zutaten

Lösung

Wir werden uns als Erstes die Überkomponente (Parent) und als Zweites die Unterkomponente (Child) anschauen.

app.component.ts
 1 import { Component } from '@angular/core';
 2 
 3 @Component({
 4   selector: 'app-root',
 5   template: `
 6     <h1>Parent</h1>
 7     <p>Parent Data: {{parentData}}</p>
 8     <app-second (dataChange)="onDataChange($event)"></app-second>
 9   `
10 })
11 export class AppComponent {
12   parentData: string = 'Initial Data';
13 
14   onDataChange(data) {
15     this.parentData = data;
16   }
17 }

Erklärung:

  • Zeile 8: Die Syntax mit den Klammern für eine Event-Bindung kennen wir schon. Nur nutzen wir hier keinen Browser-Event, sondern einen Event, den wir in der Child-Component definiert haben (siehe second.component.ts Zeile 12). Wenn das Event ausgelöst wird, rufen wir die onDataChange-Methode auf und übergeben das Event-Objekt
  • Zeilen 14-16: Methode, die aufgerufen wird, wenn das dataChange-Event ausgelöst wird
second.component.ts
 1 import {
 2     Component,
 3     Output,
 4     EventEmitter
 5 } from '@angular/core';
 6 
 7 @Component({
 8   selector: 'app-second',
 9   template: `
10     <h1>Child</h1>
11     <button (click)="sendData()">Send data to Parent</button>
12   `
13 })
14 export class SecondComponent {
15   @Output() dataChange = new EventEmitter();
16 
17   sendData() {
18     this.dataChange.emit('Child Data');
19   }
20 }

Erklärung:

  • Zeile 11: Definition einer output-Eigenschaft namens “dataChange”. Die output-Eigenschaft besitzt als Wert eine Instanz der EventEmitter-Klasse
  • Zeilen 17-19: Methode, die aufgerufen wird, wenn der Nutzer auf den Button klickt
    • Zeile 16: Die emit-Methode triggert das dataChange-Event. Der Parameter der Methode ist das Event-Objekt, das übergeben wird (siehe auch app.component.ts Zeilen 9 und 16)

Diskussion

Wir können die EventEmitter-Klasse nutzen, um eigene Events zu definieren. Diese Events können mittels der emit-Methode getriggert werden, um deren Listener zu informieren, dass das Event ausgelöst worden ist. Das machen wir uns zunutze und definieren immer unsere Output-Eigenschaften als Events, auf die eine Parent-Component hören kann, indem diese eine Event-Bindung nutzt.

Code

Code auf Github

Live Demo auf angular2kochbuch.de

Weitere Ressourcen

Code ausführen bei der Initialisierung einer Komponente

Problem

Ich möchte bei der Initialisierung meiner Komponente Code ausführen z. B. um Daten vom Server zu holen.

Zutaten

Lösung

app.component.ts
 1 import { Component, OnInit } from '@angular/core';
 2 
 3 @Component({
 4   selector: 'app-root',
 5   template: '<div>Hello World!</div>'
 6 })
 7 export class AppComponent implements OnInit {
 8   ngOnInit() {
 9     console.log('Initialization');
10   }
11 }

Erklärung:

  • Zeile 7: Hier nutzen wir das OnInit-Interface und sagen, dass unsere Komponente dieses Interface implementiert
  • Zeilen 8-10: Die ngOnInit-Methode wird bei der Initialisierung der Komponente von Angular aufgerufen. Sie ist Teil des OnInit-Interfaces

Diskussion

Angular bietet uns sogenannte “Lifecycle Hooks” an. Diese sind Methoden, die unsere Komponente implementieren kann und werden automatisch zu bestimmten Zeitpunkten von Angular aufgerufen. Der OnInit-Hook, den wir hier nutzen wird bei der Initialisierung der Komponente, nach der Konstruktorfunktion aufgerufen. Der implements OnInit Teil ist eigentlich optional, es wird aber empfohlen diesen zu nutzen, weil dann der Compiler eine Fehlermeldung ausgeben kann, falls wir den Namen der Methode falsch schreiben.

OnInit vs. Konstruktorfunktion

Auf den ersten Blick könnte man meinen, dass die zwei äquivalent sind bzw. dass wir Initialisierungscode genau so gut in den Konstruktor schreiben können. Das stimmt nur bedingt.

Die Nutzung von ngOnInit hat gewisse Vorteile. Als Erstes ist es einfacher Unit-Tests dafür zu schreiben, weil wir da besser den Zeitpunkt des Aufrufs kontrollieren können. Die Konstruktorfunktion wird bei Unit-Tests meistens von Angular automatisch aufgerufen. Die ngOnInit-Methode hingegen nicht.

Ein weiterer Vorteil von ngOnInit ist, dass diese Methode erst dann aufgerufen wird nach dem die Eigenschaft-Bindungen (@Input) der Komponente ihre Werte erhalten haben. In der Konstruktorfunktion sind die Eigenschaft-Bindungen noch undefined. Wenn wir also Zugriff auf Werte von Eigenschaft-Bindungen brauchen, müssen wir ngOnInit nutzen.

Im Allgemeinen wird empfohlen den OnInit-Hook für Initialisierungscode und die Konstruktorfunktion für Dependency Injection zu nutzen.

Code

Code auf Github

Live Demo auf angular2kochbuch.de

Weitere Ressourcen

  • Mehr Informationen über Lifecycle Hooks gibt es auf der Angular 2 Webseite

Code ausführen bei der Zerstörung (destroy) einer Komponente

Problem

Ich möchte informiert werden bevor eine Komponente von Angular zerstört wird, so dass ich z. B. die unsubscribe-Methode von einem Observable aufrufen kann.

Zutaten

Lösung

app.component.ts
 1 import { Component, OnDestroy } from '@angular/core';
 2 
 3 @Component({
 4   selector: 'app-second',
 5   template: '<div>My Name is ...</div>'
 6 })
 7 export class SecondComponent implements OnDestroy {
 8   ngOnDestroy() {
 9     console.log('Destroy');
10   }
11 }

Erklärung:

  • Zeile 7: Hier nutzen wir das OnDestroy-Interface und sagen, dass unsere Komponente dieses Interface implementiert
  • Zeilen 8-10: Die ngOnDestroy-Methode wird bei der Zerstörung der Komponente von Angular aufgerufen. Sie ist Teil des OnDestroy-Interfaces

Diskussion

Angular bietet uns sogenannte “Lifecycle Hooks” an. Diese sind Methoden, die unsere Komponente implementieren kann und werden automatisch zu bestimmten Zeitpunkten von Angular aufgerufen. Der OnDestroy-Hook, den wir hier nutzen wird z. B. aufgerufen, wenn die Komponente aus dem DOM entfernt wird. Der implements OnDestroy Teil ist eigentlich optional, es wird aber empfohlen diesen zu nutzen, weil dann der Compiler eine Fehlermeldung ausgeben kann, falls wir den Namen der Methode falsch schreiben.

Die ngOnDestroy-Methode ist der ideale Ort, um Cleanup-Code zu schreiben. Z. B. können wir hier eigene Callback-Funktionen entfernen, um Memory-Leaks zu vermeiden. Hier haben wir nur den Code für die SecondComponent gezeigt. Im Beispiel-Code auf Github wird auch die app.component.ts-Datei angepasst, so dass diese die SecondComponent aus dem DOM entfernen kann, damit die ngOnDestroy-Methode auch aufgerufen wird.

Code

Code auf Github

Live Demo auf angular2kochbuch.de

Weitere Ressourcen

  • Mehr Informationen über Lifecycle Hooks gibt es auf der Angular 2 Webseite

Rezepte für ngFor-Listen

In diesem Kapitel befinden sich Rezepte, die uns bei der Arbeit mit Listen helfen, die in der View mittels der NgFor-Direktive angezeigt werden. Wie man solche Listen anzeigen kann, wird im Rezept “Liste von Daten anzeigen” dargestellt.

Mit dem Index von ngFor-Elementen arbeiten

Problem

Ich möchte den Index der ngFor-Elemente in der View anzeigen.

Zutaten

Lösung

app.component.ts
 1 ...
 2 
 3 @Component({
 4   selector: 'app-root',
 5   template: `
 6     <ul>
 7       <li *ngFor="let user of users; let i = index">
 8         Index: {{i}},
 9         Name: {{user.firstname}} {{user.lastname}}
10       </li>
11     </ul>
12   `
13 })
14 
15 ...

Erklärung:

Die Anpassungen betreffen nur die template-Eigenschaft der Komponente. Der Rest bleibt gleich.

  • Zeile 7: Definiert eine lokale Variable “i”, die den Index für das aktuelle Element referenziert. In diesem Fall ist index ein spezielles ngFor-Konstrukt
  • Zeile 8: Die lokale Variable “i” für den Index wird mittels Interpolation angezeigt

Diskussion

Hier zeigen wir den Index nur in der View an. Natürlich können wir die lokale Variable “i” auch einer Methode übergeben oder diese nutzen, um etwas zu berechnen. Z. B. können wir in Zeile 8 eine Eins zu der Variable “i” addieren, so dass die Anzeige mit 1 statt 0 beginnt. Der Ausdruck dafür wäre {{i + 1}}.

Code

Code auf Github. Dort werden auch die weiteren möglichen Schreibweisen der NgFor-Direktive mit Index gezeigt.

Live Demo auf angular2kochbuch.de

Weitere Ressourcen

Gerade und ungerade ngFor-Elemente unterscheiden

Problem

Ich möchte, dass gerade Elemente meiner Liste eine andere Farbe als ungerade erhalten.

Zutaten

Lösung

app.component.ts
 1 ...
 2 
 3 @Component({
 4   selector: 'app-root',
 5   styles: [
 6     '.red { color: red; }',
 7     '.green { color: green; }'
 8   ],
 9   template: `
10     <ul>
11       <li *ngFor="let user of users; let isEven = even; let isOdd = odd"
12           [ngClass]="{red: isOdd, green: isEven}">
13         Name: {{user.firstname}} {{user.lastname}}
14       </li>
15     </ul>
16   `
17 })
18 
19 ...

Erklärung:

Die Anpassungen betreffen nur die template-Eigenschaft der Komponente. Der Rest bleibt gleich.

  • Zeilen 6-7: Definition von CSS-Klassen
  • Zeile 11: Definiert lokale Variablen “isEven” und “isOdd” die, je nachdem ob das aktuelle Element gerade oder ungerade ist, true oder false sind. In diesem Fall sind even und odd spezielle ngFor-Konstrukte
  • Zeile 12: Hier werden, je nachdem, ob das aktuelle Element gerade oder ungerade ist, die Klassen “green” oder “red” gesetzt

Diskussion

Hier nutzen wir die Information, ob das aktuelle Element gerade oder ungerade ist, um die richtige CSS-Klasse zu setzen. Natürlich können wir die lokalen Variablen “isOdd” und “isEven” auch einer Methode übergeben oder diese nutzen, um z. B. nur gerade oder nur ungerade Elemente anzuzeigen.

Code

Code auf Github. Dort werden auch die weiteren möglichen Schreibweisen der NgFor-Direktive mit even/odd gezeigt.

Live Demo auf angular2kochbuch.de

Weitere Ressourcen

Das erste und das letzte ngFor-Element finden

Problem

Ich möchte das erste und das letzte Element meiner Liste farblich hervorheben.

Zutaten

Lösung

app.component.ts
 1 ...
 2 
 3 @Component({
 4   selector: 'app-root',
 5   styles: [
 6       '.first { background-color: red; }',
 7       '.last { background-color: green; }'
 8   ],
 9   template: `
10     <ul>
11       <li *ngFor="let user of users; let isLast = last; let isFirst = first"
12           [ngClass]="{first: isFirst, last: isLast}">
13         Name: {{user.firstname}} {{user.lastname}}
14       </li>
15     </ul>
16   `
17 })
18 
19 ...

Erklärung:

Die Anpassungen betreffen nur die template-Eigenschaft der Komponente. Der Rest bleibt gleich.

  • Zeilen 6-7: Definition von CSS-Klassen
  • Zeile 11: Definiert die lokalen Variablen “isLast” und “isFirst”. Die isLast-Variable ist true, wenn das aktuelle Element das Letzte in der Liste ist, ansonsten ist sie false. Die isFirst-Variable ist true, wenn das aktuelle Element das Erste in der Liste ist, ansonsten ist sie false. In diesem Fall sind last und first spezielle ngFor-Konstrukte
  • Zeile 12: Hier wird die Klasse “first” gesetzt, falls das aktuelle Element das Erste der Liste ist oder “last” falls es das letzte Element der Liste ist

Diskussion

Hier nutzen wir die Information, ob das aktuelle Element das Erste oder das Letzte in der Liste ist, um die entsprechenden Elemente farblich hervorzuheben. Wie auch in den vorherigen zwei Rezepten können wir hier die Information, ob ein Element das Erste oder das Letzte einer Liste ist, z. B. an eine Methode übergeben.

Code

Code auf Github. Dort werden auch die weiteren möglichen Schreibweisen der NgFor-Direktive gezeigt.

Live Demo auf angular2kochbuch.de

Weitere Ressourcen

Die Performance mit trackBy verbessern

Problem

Ich habe eine große Liste von Objekten, die ich mit NgFor anzeige und ich möchte die Liste durch eine Liste ersetzen, die größtenteils die gleichen Objekte enthält.

Zutaten

Lösung

app.component.ts
 1 import { Component } from '@angular/core';
 2 
 3 interface User {
 4   firstname: string,
 5   lastname: string,
 6   id: number
 7 }
 8 
 9 const users: Array<User> = [
10   {firstname: 'Max', lastname: 'Mustermann', id: 0},
11   {firstname: 'John', lastname: 'Doe', id: 1}
12 ];
13 
14 @Component({
15   selector: 'app-root',
16   template: `
17     <ul>
18       <li *ngFor="let user of users; trackBy:trackById">
19         Name: {{user.firstname}} {{user.lastname}}
20       </li>
21     </ul>
22   `
23 })
24 export class AppComponent {
25   users = users;
26 
27   constructor() {...}
28 
29   trackById(index: number, user: User) {
30     return user.id;
31   }
32 }

Erklärung:

Damit wir “trackBy” effizient nutzen können, müssen die Objekte in unserem Array eine Eigenschaft, deren Wert für jedes Objekt eindeutig ist, besitzen. Wir haben hier eine Eigenschaft namens “id” benutzt.

  • Zeilen 9-12: Array mit User-Objekten die einen eindeutigen Wert für die id-Eigenschaft besitzen
  • Zeile 18: Hier nutzen wir die spezielle Eigenschaft “trackBy” der NgFor-Direktive mit der trackById-Methode der Klasse (siehe Zeilen 29-31)
  • Zeilen 29-31: Methode, die wir in Kombination mit der trackBy-Eigenschaft in Zeile 18 nutzen

Diskussion

Es kann zu Performance-Problemen kommen, wenn wir NgFor mit großen Listen nutzen und wir später die Liste durch eine Neue ersetzen. In solchen Fällen können wir trackBy nutzen. Angular wird dann nur die DOM-Elemente ersetzen, die sich tatsächlich geändert haben. Ohne trackBy muss Angular die komplete Liste im DOM neu generieren, da es nicht wissen kann, dass sich ggf. viele Elemente gar nicht geändert haben.

Damit Angular erkennen kann, welche Elemente sich geändert haben, müssen wir eine trackBy-Methode (hier “trackById”) definieren. Diese erhält den Index und ein Listen-Element als Parameter und gibt den Wert der eindeutigen Eigenschaft (hier “user.id”) zurück. Wenn jetzt die Liste ersetzt wird, wird Angular durch die neue Liste gehen und für jedes Element die trackBy-Methode aufrufen. Falls der zurückgegebene Wert schon bekannt ist (z. B. das Element mit ID 1 war schon in der alten Liste), wird Angular das Element mit dem schon bekannten Element vergleichen. Wenn das alte sich vom neuem Element unterscheidet, wird das DOM aktualisiert. Falls diese gleich sind, bleibt das DOM so wie es ist. Elemente, die noch nicht bekannt sind, werden hinzugefügt und alte Elemente, die sich nicht mehr in der Liste befinden, werden aus dem DOM entfernt.

Das Code-Beispiel auf Github ist ausführlicher als das Beispiel hier. Dort können wir auch sehen, wie sich in jedem Fall das DOM verändert, wenn wir auf den “New List” Button klicken. Am einfachsten ist es, in den Chrome DevTools sich die Elemente im DOM anzuschauen und gleichzeitig auf den Button zu klicken. In den Varianten ohne “trackBy” wird sich die komplette Liste ändern und in den Varianten mit “trackBy” nur das zweite Element der Liste.

Code

Code auf Github. Dort werden auch die weiteren möglichen Schreibweisen der NgFor-Direktive mit trackBy gezeigt.

Live Demo auf angular2kochbuch.de

Appendix A: Template-Syntax

Angular 2 nutzt Templates, die es uns erlauben, die Views der Komponenten zu definieren. Diese Templates unterstützen den Großteil der HTML-Syntax und sie unterstützen auch eine spezielle Syntax, die es uns z. B. ermöglicht, Daten, die sich in Komponenten befinden, dem Nutzer anzuzeigen. Wir werden gleich sehen, wie die spezielle Angular-Syntax für Templates aussieht und was wir damit machen können. Hier handelt es sich nur um eine Kurzfassung mit den wichtigsten Informationen. Die offizielle Angular Dokumentation gibt weitere Details über die Template-Syntax.

Template-Ausdruck

Angular erlaubt die Nutzung von Ausdrücken in den Templates. Diese sind ähnlich zu JavaScript-Ausdrücken, allerdings sind in Template-Ausdrücken nicht alle JavaScript-Konstrukte erlaubt. Z. B. sind Konstrukte mit Nebeneffekten nicht erlaubt. Es ist also nicht möglich, Zuweisungen, “new”, “++”, usw. zu nutzen. Des Weiteren hat der Operator “|” in Angular-Templates eine andere Bedeutung als in JavaScript. Hier wird er benutzt, um das Resultat eines Ausdrucks zu transformieren bevor dieses in der View benutzt wird. Angular bezeichnet den Operator als “Pipe-Operator”. Wie dieser funktioniert wird in den entsprechenden Rezepten gezeigt. Eine weitere Besonderheit von Template-Ausdrücken ist die Existenz des sogenannten “Elvis-Operators” (?.). Dieser kann uns helfen, wenn wir im Template mit Objekten arbeiten, die null oder undefined sein könnten. Da Methodenaufrufe erlaubt sind, müssen wir als Entwickler selbst darauf achten, dass der Aufruf keine Nebeneffekte hat. Wichtig zu beachten ist der Kontext, in dem die Template-Ausdrücke evaluiert werden. Wir können nur Eigenschaften der Komponente zu der die View gehört und lokale Variablen, die im Template definiert sind, nutzen.

Template-Anweisung

Template-Anweisungen erlauben uns das Aktualisieren des Zustands unserer Anwendung. Sie werden bei Event-Bindungen benutzt und können Nebeneffekte haben. Sie sind also eine Reaktion auf Events, die durch den Nutzer ausgelöst werden, z. B. click-Events. Wie auch Template-Ausdrücke nutzen Template-Anweisungen eine JavaScript-ähnliche Syntax. Aber auch hier ist nicht alles erlaubt, was in einer JavaScript-Anweisung erlaubt ist. Z. B. sind “new”, “++” und bit-Operationen nicht erlaubt. Auch die Nutzung von globalen Variablen ist nicht erlaubt. Nur Eigenschaften der Komponente zu der die View gehört und lokale Variablen, die im Template definiert sind, können benutzt werden.

Datenbindung

Angular bietet verschiedene Möglichkeiten, Daten, die sich in einer Komponente befinden, in der dazugehörige View zu nutzen. Damit man die Daten nutzen kann, müssen diese als Eigenschaften der Komponente definiert sein. Das heißt für uns, sie müssen Eigenschaften des this-Wertes der Komponenten-Klasse sein.

Wir unterscheiden zwischen “Einweg-Datenbindung” und “beidseitiger Datenbindung”. Bei der Einweg-Datenbindung fließen die Daten entweder von der View in die Komponente oder von der Komponente in die View. Bei der beidseitigen Datenbindung fließen die Daten mit nur einem Bindungskonstrukt in beide Richtungen. Unter Bindungskonstrukt verstehen wir die Syntax, die wir brauchen, um Angular zu signalisieren, dass hier Daten gebunden werden sollen. Jetzt werden wir sehen, wie wir die verschiedenen Bindungskonstrukte nutzen können und was diese bewirken.

Daten mittels Interpolation nutzen

Wir können Template-Ausdrücke interpolieren, indem wir doppelte geschweifte Klammern {{…}} nutzen. Der Template-Ausdruck wird zwischen die Klammern geschrieben. Bevor der Ausdruck angezeigt oder einem HTML-Attribut zugewiesen werden kann, wird er evaluiert. Das Resultat der Evaluation wird in einen String umgewandelt und zuletzt mit den Strings, die sich links und rechts von den Klammern befinden, konkateniert. Bei der String-Umwandlung nutzt Angular die toString-Methode. Darum ist es selten sinnvoll, bei der Interpolation eine Referenz auf ein Objekt zu nutzen. Wenn man Daten von Objekten anzeigen möchte, muss man im Template auf die einzelnen Eigenschaften des Objekts zugreifen.

Angenommen wir haben eine Komponenteneigenschaft namens “data” mit einem String-Wert von “test data”. Dann können wir Interpolation nutzen, um den Wert anzuzeigen. Das geht so:

<div>My data: {{data}}</div>

Was der Nutzer auf seinem Bildschirm sehen wird, ist “My data: test data”. Komplexere Ausdrücke sind auch möglich:

<div> 1 + 2 is {{1 + 2}}</div>

Hier wird dem Nutzer “1 + 2 is 3” angezeigt. Das Resultat der Interpolation kann auch als Wert für ein HTML-Attribut benutzt werden:

<img src="{{url}}" />

Hier wird angenommen, dass unsere Komponente eine Eigenschaft namens “url” besitzt, die als Wert eine URL für das src-Attribut besitzt. Die Interpolation ist eine Einweg-Bindung, bei der die Daten aus der Komponente in die View fließen.

Eigenschaft-Bindung

Diese Art der Bindung wird bei Eigenschaften von (Unter-) Komponenten, Direktiven und DOM-Elementen benutzt. Der Name der Eigenschaft wird als Ziel der Bindung bezeichnet.

Bei der Eigenschaft-Bindung reden wir von einer Einwegbindung. Hierbei fließen die Daten aus einem Template-Ausdruck in das Ziel der Bindung. Eigenschaft-Bindungen können wir mit eckigen Klammern […] definieren. Hier ein Beispiel:

<img [src]="url"/>

Der Namen zwischen der Klammern (hier “src”) ist das Ziel der Bindung. Auf der rechten Seite der Eigenschaft-Bindung befindet sich ein Template-Ausdruck, der evaluiert und dann als Wert der src-Eigenschaft gesetzt wird. Alternativ können wir auch die Eigenschaft-Bindung mit der bind-Syntax realisieren:

<img bind-src="url"/>

Hier wird der Präfix “bind-“, anstelle der eckigen Klammern verwendet. Das Resultat ist dasselbe, egal welche Syntax wir nutzen. Es ist also Geschmackssache, ob man Klammern oder “bind-“ nutzt.

Event-Bindung

Bis jetzt haben wir uns mit Bindungen beschäftigt, die es uns erlaubt haben, Daten der Komponente in der View zu nutzen. Jetzt geht es um eine Art der Bindung, die zwar auch eine Einwegbindung ist, die aber in die andere Richtung geht. Hier fließen Daten von der View in die Komponente. Dafür nutzen wir eine spezielle Syntax mit Klammern (…). Zwischen die Klammern kommt der Name eines Events. Wir können DOM-Events wie z. B. “click”, “change”, etc. nutzen, oder wir können eigene Events definieren. Eigene Events werden in Komponenten und Direktiven definiert. Hier ein Beispiel für das click-Event:

<button type="button" (click)="doSomething()">Do</button>

Der Name zwischen den Klammern wird als das Ziel-Event bezeichnet. In unserem Beispiel ist “click” das Ziel-Event. Wenn also der Nutzer auf den Button klickt, wird die doSomething-Methode der Komponenten aufgerufen. Alternativ können wir auch folgende Syntax verwenden:

<button type="button" on-click="doSomething()">Do</button>

Das “on-“ Präfix wird statt der Klammern benutzt. Auch hier ist es Geschmackssache, welche Syntax verwendet wird. Das Resultat ist gleich.

Event-Objekt

Wenn der Nutzer ein Event auslöst, gibt es in der Regel auch ein dazugehöriges Event-Objekt. Wir können mittels einen speziellen Parameters namens “$event” auf das Event-Objekt zugreifen. Der $event-Parameter referenziert das Event-Objekt. Wie dieses Objekt aussieht, hängt vom Event-Typ ab. Bei DOM-Events erhalten wir das dazugehörige DOM-Event-Objekt. Bei eigenen Events definiert der Entwickler, wie das Event-Objekt aussieht. So bekommen wir Zugriff auf ein Event-Objekt:

<button type="button" (click)="doSomething($event)">Do</button>

In der doSomething-Methode können wir das Event-Objekt nutzen. Da wir bei der Event-Bindung Template-Anweisungen anstelle von Template-Ausdrücken nutzen, gibt es auch eine weitere Möglichkeit, das Objekt zu erhalten: mittels einer Zuweisung.

<button type="button" (click)="myEvent = $event">Do</button>

Jetzt haben wir in der Komponente über die myEvent-Eigenschaft Zugriff auf das Event-Objekt. Statt des $event, können wir auch direkt einen Wert zuweisen oder der doSomething-Methode einen anderen Wert übergeben:

<button type="button" (click)="name = 'Max'">Do</button>
<button type="button" (click)="doSomething('Max')">Do</button>

Jetzt beinhaltet die name-Eigenschaft den Wert 'Max' und die doSomething-Methode bekommt 'Max' als Wert übergeben. Natürlich können auch lokale Variablen oder Eigenschaften als Wert übergeben bzw. zugewiesen werden.

Beidseitige Datenbindung

Um eine beidseitige Datenbindung nutzen zu können, benötigen wir spezielle Direktiven und ein weiteres Bindungskonstrukt. Angular bietet von Haus aus eine solche Direktive, und zwar die NgModel-Direktive. Das Bindungskonstrukt sind Klammern umgeben von eckigen Klammern [(…)]. Wie man vermutlich aus der Syntax schon erkennen kann ist eine beidseitige Datenbindung eine Kombination einer Eigenschaft- und einer Event-Bindung. Beidseitige Bindungen funktionieren nur mit Eigenschaften von Komponenten und Direktiven aber nicht mit DOM-Eigenschaften. Hier ein Beispiel mit einem Eingabefeld und der NgModel-Direktive:

<input type="text" [(ngModel)]="name"/>

Der Wert der name-Eigenschaft wird als Wert des Eingabefeldes gesetzt. Wenn der Nutzer den Inhalt des Eingabefeldes verändert, wird sich auch der Wert der name-Eigenschaft verändern. Es gibt auch hier eine Alternativ-Syntax, und zwar mit dem “bindon-“ Präfix:

<input type="text" bindon-ngModel="name"/>

Wichtig bei der beidseitigen Bindung sind die Namen der Input- und der Output-Eigenschaften. Im Fall der NgModel-Direktive hat die Input-Eigenschaft den Namen “ngModel” und die Output-Eigenschaft den Namen “ngModelChange”. Wir können also die beidseitige Bindung aufspalten, wenn wir das möchten:

<input type="text" [ngModel]="name" (ngModelChange)="name = $event"/>

Attribut-Bindung

Es gibt HTML-Attribute, die keine dazugehörige DOM-Eigenschaft besitzen. Für solche Fälle bietet Angular die Attribut-Bindung an. Eine Attribut-Bindung ist ähnlich einer Eigenschaft-Bindung, sprich in beiden Fällen nutzen wir eckige Klammern. Der Unterschied ist, dass wir hier keinen Eigenschaftsnamen haben, sondern ein Keyword attr und dann den Namen des Attributs. Als Beispiel nutzen wir das colspan-Attribut:

<table>
  <tr>
    <td [attr.colspan]="columnSpan">Num of columns</td>
  </tr>
</table>

Diese Schreibweise müssen wir für alle Attribute nutzen, die keine dazugehörige DOM-Eigenschaft besitzen. Es wird empfohlen immer die Eigenschaft-Bindung zu nutzen, außer es gibt keine andere Wahl. Bei Unsicherheit können wir versuchen eine Eigenschaft zu binden. Falls diese nicht existiert, wird Angular eine Exception werfen. Dann wissen wir, dass wir eine Attribut-Bindung nutzen müssen.

Klassenbindung

Eine Klassenbindung ist ähnlich einer Attribut-Bindung. Der Unterschied: hier wird das Keyword class statt attr benutzt und nach dem Keyword kommt ein Klassenname. Hier ein Beispiel:

<div [class.red]="isRed"></div>

Hier wird die Klasse “red” konditional gesetzt. Falls die Komponenten-Eigenschaft “isRed” true ist, wird die Klasse gesetzt. Falls diese false ist, wird die Klasse entfernt. Diese Schreibweise ist ideal, wenn wir nur eine Klasse konditional setzen möchten. Falls wir mehrere Klassen setzen möchten, ist es besser die NgClass-Direktive zu nutzen.

Style-Bindung

Bei der Style-Bindung nutzten wir das Keyword style und nach dem Keyword kommt der Name des Styles. Genau wie die Klassenbindung ist die Style-Bindung ideal, wenn wir nur einen Style konditional setzen möchten. Für mehrere Styleänderungen gibt es die NgStyle-Direktive. Hier ein Beispiel:

<div [style.color]="isRed ? 'red' : 'black'"></div>

Falls “isRed” true ist, wird das color-Style den Wert red haben. Falls es false ist, wird das color-Style den Wert black haben. Manche Styles besitzen auch eine Maßeinheit wie z. B. “px”, “em” usw. So können wir auch die Maßeinheit mit definieren:

<div [style.width.px]="isBig ? 150 : 50"></div>

Wenn “isBig” true ist, wird die Breite des Elementes 150px sein. Falls es false ist, wird die Breite 50px sein.

Lokale Variablen

Wie schon erwähnt, können wir in Templates lokale Variablen definieren und nutzen. Das Wort “lokal” bedeutet in diesem Fall, dass wir die Variablen nur im Template nutzen können. Wir haben z. B. in der Klasse der Komponente keinen direkten Zugriff darauf. Es zwei Arten von lokalen Variablen: Template-Referenzvariablen (template reference variables) und Template-Eingabevariablen (template input variables). Diese unterscheiden sich in der Syntax und dem Wert der Variablen.

Template-Referenzvariablen

Diese lokalen Variablen besitzen als Wert eine Referenz auf ein DOM-Element oder auf eine Direktive (zur Erinnerung: Komponenten sind auch Direktiven). Um Template-Referenzvariablen zu definieren, können wir eine Raute (#) nutzen. Hier ein Beispiel:

DOM-Element Referenz
<div #local></div>

Alternativ können wir Template-Referenzvariablen auch mit dem “ref-“ Präfix definieren. Hier ein Beispiel:

DOM-Element Referenz mit ref-
<div ref-local></div>

In beiden Fällen ist der Wert von “local” das div-Element. Wie oben erwähnt kann eine Template-Referenzvariable auch eine Referenz auf eine Direktive sein. Damit die lokale Variable eine Referenz auf eine Direktive sein kann, müssen wir eine Zuweisung nutzen und die Direktive muss die exportAs-Eigenschaft setzen. Natürlich muss auf dem Tag auch eine entsprechende Direktive definiert sein. Hier ein Beispiel:

<form #local="ngForm"></form>

Hier besitzt “local” eine Referenz auf die NgForm-Direktive als Wert. Wichtig zu beachten: NgForm nutzt “exportAs” mit 'ngForm' (ein String) als Wert und wir nutzen den Namen “ngForm” bei der Zuweisung. Ein anderer Name würde zu einem Fehler führen. Ob eine Direktive einen Wert für die exportAs-Eigenschaft setzt, können wir in der Regel in der Dokumentation nachlesen. Auch hier können wir das “ref-“ Präfix nutzen:

Direktive Referenz mit ref-
<form ref-local="ngForm"></form>

Template-Eingabevariablen

Der Wert der Template Eingabevariablen wird von Direktiven gesetzt. Diese Art lokaler Variablen funktioniert nur bei strukturellen Direktiven, sprich Direktiven, die ein Template nutzen, wie z. B. NgFor. In diesem Fall hat das Wort “lokal” eine leicht veränderte Bedeutung. Diese Variablen können nicht im gesamten Angular-Template benutzt werden, sondern nur im Template der Direktive, die die Variablen definiert. Hier ein Beispiel:

Implizite Eingabe-Variable mit NgFor
<li *ngFor="let local of objectsArray"></li>

Das li-Element definiert das Template für die NgFor-Direktive. In diesem Fall ist der Wert von “local” eine Referenz auf das aktuelle Element im Array und kann nur innerhalb des li-Elements benutzt werden. Die Eingabevariable ist implizit, da wir für “local” keine Zuweisung nutzen. Eine Direktive kann auch mehrere Template-Eingabevariablen definieren. Hier noch ein Beispiel mit NgFor und einer weiteren Variablen:

Explizite Eingabe-Variable mit NgFor
<li *ngFor="let l of objects; let local = index"></li>

Hier ist der Wert von “local” der Index des aktuellen Elements. Wie oben ist “l” das aktuelle Element. Auch hier kann “local” nur innerhalb des li-Elementes benutzt werden. Die Variable ist explizit, da wir für “local” eine Zuweisung nutzen. Die rechte Seite der Zuweisung ist der Name einer Eigenschaft. Die Direktive ist dafür zuständig diesen Namen als Eingabevariable zu definieren. Wir können nur Namen nutzen, die die Eigenschaft in den Kontext des Templates setzt (das macht diese zu Eingabevariablen). Wir können also nicht beliebige Eigenschaften einer Direktive bei der Zuweisung nutzen.

Appendix B: angular-cli

In den Rezepten nutzen wir angular-cli hauptsächlich, um einen Webserver für den Beispiel-Code zu starten. Das Tool kann aber noch einiges mehr. Wir wollen uns hier angular-cli ein bisschen genauer anschauen. Konkreter werden wir uns die wichtigsten Kommandos anschauen und uns einen groben Überblick über die Verzeichnisstruktur, die angular-cli generiert, verschaffen.

Der Hauptvorteil von angular-cli ist, dass wir damit mit wenig Aufwand einen Entwicklungsprozess für Angular 2 Anwendungen komplett mit Linting, Konfigurationsdateien für Tests, einem Webserver mit Live-Reload, einem Build-Prozess, um die TypeScript-Dateien zu kompilieren, und mehr, aufbauen können. Würden wir das alles manuell machen, wären wir sicherlich mehrere Stunden damit beschäftigt.

Installation

Als Erstes müssen wir Node.js und npm installieren, damit wir im zweiten Schritt angular-cli installieren können. Am einfachsten können wir Node.js installieren, indem wir es von der offiziellen Webseite herunterladen. Bei der Installation von Node.js wird npm mitinstalliert. Jetzt können wir angular-cli installieren mit:

1 npm install -g angular-cli@1.0.0-beta.19

Wir installieren angular-cli global und können es in jedem Angular 2 Projekt nutzen.

Jetzt können wir das Tool für unsere Projekte nutzen.

Kommandos

Folgende Kommandos werden wir uns detaillierter anschauen:

  • new
  • init
  • serve
  • build
  • lint
  • test
  • generate

Das sind zwar nicht alle Kommands, sie sollten aber für die meisten Projekte reichen. Mit dem help-Kommando können wir uns alle Kommandos von angular-cli inklusive der unterstützten Optionen anschauen.

1 ng help

Um mehr Informationen über ein Kommando zu bekommen, können wir die --help Option nutzen:

1 ng --help kommandoName

Wir werden uns nicht jede Option für jedes Kommando anschauen. Wer mehr erfahren möchte, kann die help-Option nutzen oder im Github-Repository von angular-cli nachschauen. Allerdings sind nicht alle Kommandos bzw. Optionen gleich gut dokumentiert.

new

1 ng new projektName

Dieses Kommando wird ein Verzeichnis mit dem Namen “projektName” erzeugen, darin die nötigen Verzeichnisse/Dateien anlegen und alle Abhängigkeiten mittels npm installieren. Standardmäßig wird das neue Verzeichnis als git-Repository initialisiert. Das können wir mit der --skip-git-Option verhindern. Falls wir uns schon in einem git-Repository befinden, wird angular-cli kein neues Repository initialisieren.

init

Das init-Kommando macht das gleiche wie das new-Kommando, aber es initialisiert die Anwendung im aktuellen Verzeichnis.

1 ng init --name projektName

Falls --name projektName nicht angegeben wird, wird der Name des Verzeichnisses als Projektname benutzt.

build

Das build-Kommando kompiliert/baut die Anwendung und schreibt die JavaScript-Dateien und SourceMaps in das dist-Verzeichnis.

1 ng build

Die gebaute Version der Anwendung ist für die Entwicklung gedacht. Diese ist also nicht minimiert.

1 ng build --prod

Baut eine minimierte Version der Anwendung, die wir später auch für das Produktivsystem nutzen können.

serve

Das serve-Kommando kompiliert/baut die Anwendung und startet einen Webserver mit Live-Reload.

1 ng serve

Standardmäßig nutzt der Webserver Port 4200 und hört auf alle Interfaces. Mit den Optionen --port und --host können wir das ändern.

lint

Das tslint-Tool ist in angular-cli integriert und ermöglicht uns das Testen des Codes auf mögliche Verletzungen von Best Practices und Code-Konventionen. Tslint überprüft den TypeScript-Code. Es gibt aber auch spezielle Regeln für Angular 2. Diese werden von codelyzer zur Verfügung gestellt.

1 ng lint

Falls nötig, können wir die Regeln für tslint und codelyzer in der tslint.json-Datei anpassen bzw. ergänzen. Beide Tools unterstützen noch weitere Regeln, die sich nicht in der tslint.json-Datei befinden. Es ist ggf. lohnenswert, sich die Webseiten der Tools anzuschauen, um zu sehen was es sonst noch an Regeln gibt und ob es sinnvoll wäre, diese zu nutzen.

test

Das test-Kommando startet die Unit-Tests für das Projekt. Bei der Initialisierung wurden schon die nötigen Konfigurationsdateien angelegt und die Abhängigkeiten installiert. Wir brauchen also nur Tests zu schreiben und dann:

1 ng test

aufzurufen.

Bevor die Tests aufgerufen werden, wird die Anwendung gebaut. Nach dem Bauen startet karma Chrome und führt die Unit-Tests aus. Diese werden standardmäßig mit Jasmine geschrieben. Wenn die Tests durchgelaufen sind, bleibt der Browser offen und wartet auf Änderungen. Bei jeder Änderung führt karma die Tests erneut aus. Die Konfiguration für karma befindet sich in der karma.conf.js-Datei im config-Verzeichnis.

generate

Mit dem generate-Kommando können wir Komponenten, Direktiven, Services usw. generieren. Dabei erzeugt angular-cli alle nötige Dateien mit dem angegebenen Namen und befüllt diese mit dem richtigen boilerplate-Code.

Um eine neue Komponente zu generieren, können wir folgenden Befehl nutzen:

1 ng generate component komponenten-name

Standardmäßig wird für jede Komponente ein Verzeichnis angelegt. Mit der --flat Option können wir das verhindern. Die Komponente wird in dem Verzeichnis generiert, in dem wir den Befehl aufrufen oder in “./src/app”, falls der Befehl außerhalb des app-Verzeichnises ausgeführt wird.

Für die Komponente wird eine .ts-, eine .spec.ts-, eine .css- und eine .html-Datei generiert. Wenn wir lieber inline-HTML bzw. -CSS nutzen möchten, können wir die Optionen --inline-template bzw. --inline-style nutzen. Dann werden die .html- und .css-Dateien nicht generiert. Die neu generierte Komponente wird auch automatisch im AppModule deklariert.

Im Allgemeinen sieht das Kommando so aus:

1 ng generate blueprint name

Oben haben wir “blueprint” durch “component” ersetzt. Es gibt weiter blueprint für Services, Direktiven, Routes, usw. Um es kurz zu halten, werden wir uns diese hier nicht anschauen. Der Aufruf für die weiteren Blueprints ist ähnlich zu dem für die Komponenten, allerdings haben die meisten anderen Blueprints weniger Optionen. Am besten ist es, ein Dummy-Projekt zu initialisieren und die verschiedenen Kommandos von angular-cli zu testen.

Verzeichnisstruktur

Die Verzeichnisstruktur, die von angular-cli generiert wird, folgt den Best Practices aus dem Angular Style Guide. Wir verschaffen uns jetzt eine grobe Übersicht.

angular-cli Verzeichnisstruktur
 1 |- dist/
 2 |- e2e/
 3 |- node_modules/
 4 |- src/
 5 |----- app/
 6 |---------- shared/
 7 |---------------- index.ts
 8 |---------- index.ts
 9 |---------- app.component.css|html|spec.ts|ts
10 |---------- app.module.ts
11 |----- assets/
12 |----- environments/
13 |----- favicon.ico
14 |----- index.html
15 |----- main.ts
16 |----- polyfills.ts
17 |----- styles.css
18 |----- test.ts
19 |----- tsconfig.json
20 |----- typings.d.ts
21 |- .editorconfig
22 |- .gitignore
23 |- angular-cli.json
24 |- karma.conf.js
25 |- package.json
26 |- protractor.conf.js
27 |- README.md
28 |- tslint.json

/

Das Hauptverzeichnis für das Angular 2 Projekt. Außer Verzeichnissen beinhaltet es Konfigurationsdateien für verschiedene Tools wie angular-cli, tslint und npm.

dist/

Wird erst generiert nachdem wir die Anwendung gebaut haben. Dort befindet sich die kompilierte Anwendung.

e2e/

Verzeichnis für die End-to-End-Tests.

node_modules/

Hier befinden sich alle Abhängigkeiten, die mittels npm installiert worden sind.

src/

In diesem Verzeichnis befindet sich der Code für unsere Anwendung und Konfigurationsdateien, die vom TypeScript-Compiler (tsconfig.json, typings.d.ts) gebraucht werden.

src/app/

Das Verzeichnis für unseren Code. Hier werden wir bei der Entwicklung die meiste Zeit verbringen. Standardmäßig wird eine Komponente und ein Verzeichnis namens “shared” generiert. In dieses Verzeichnis gehört Code, den wir in mehreren Komponenten nutzen wollen.

src/assets

In dieses Verzeichnis gehören alle Ressourcen, die wir für unsere Anwendung brauchen, die aber nicht Teil des Build-Prozesses sind, wie z. B. Bilder und Fonts.

src/environments

Die Dateien in diesem Verzeichnis werden für den Build-Prozess benötigt. Wenn wir die --prod Option nutzen, wird die environment.prod.ts-Datei mit der environment.js-Datei benutzt. Standardmäßig wird die environment.ts-Datei benutzt.

src/styles.css

In dieser Datei können wir globale Styles für die Anwendung definieren.

Glossar

Angular 2 Anwendung: Eine Angular 2 Anwendung ist ein Baum von Komponenten und hat immer mindestens eine Komponente. Die Komponente die immer vorhanden ist, ist die Hauptkomponente und diese wird dem bootstrap-Array des Hauptmoduls übergeben. Die Hauptkomponente bildet die Wurzel des Baums.

Angular-Modul: Angular-Module sind Klassen mit denen wir unsere Anwendung in sinnvollen Blöcken von Features/Funktionalität aufspalten können. Jedes Modul deklariert seine Komponenten, Direktiven und Pipes und kann funktionalität von weiteren Modulen importieren und auch exportieren.

barrel: Ein barrel in Angular 2 ist eine Datei die mehrere Module importiert und gewisse Teile der Module exportiert. Es ist eine einfache Möglichkeit mit möglichst wenige Import-Zeilen, viel Funktionalität zu importieren. In der Regel importieren wir Klassen, Methoden usw. aus einem barrel wie z. B. @angular/core statt die Abhängigkeiten direkt aus der Datei, die diese definiert zu importieren.

bootstrap: Das initialisieren einer Angular 2 Anwendung. Der Initialisierungsprozess beginnt in dem wir eine bootstrap-Funktion aufrufen und das Hauptmodul als Parameter übergeben.

Datenbindung: Auf Englisch “data binding” ist die Verbindung zwischen Daten in einem Angular-Template und entsprechende Daten in einer Komponente oder Direktive. Datenbindungen können auf verschiedenen Weisen entstehen z. B. durch Interpolation, Event-Bindung mittels Klammern (…) oder Eigenschaft-Bindung mittels eckigen Klammern […].

Decorator: Ist eine Funktion die Metadaten zu einer Klasse, deren Methoden und Eigenschaften und Methodenparameter hinzufügen kann. Decorators sind ein TypeScript Feature und fangen immer mit einem “@” an.

Dependency Injection (DI): Ist ein Design Pattern, um Abhängigkeiten zu verwalten. Angular nutzt DI, um Abhängigkeiten wie z. B. Services zu instantiieren und der Komponente zu übergeben.

Direktive: Eine Direktive in Angular 2 ist ein UI-Baustein, den wir nutzen können um HTML-Elemente zu definieren, zu ändern und deren Verhalten zu beeinflussen. Sie sind die wesentlichen Bausteine von Angular. Direktiven gehören zu eine von drei Kategorien: Komponenten, Attribut-Direktive und strukturelle-Direktive.

Hauptkomponente: Die Komponente, die wir dem bootstrap-Array des Hauptmoduls übergeben. Siehe auch Komponente und Hauptmodul.

Hauptmodul: Ist das Angular-Modul, das wir der bootstrap-Funktion übergeben und es definiert die Hauptkomponente der Anwendung. Siehe auch Angular-Modul.

input-Eigenschaft: Ist eine Eigenschaft einer Komponente/Direktive, die die Überkomponente nutzen kann, um Daten der Komponente/Direktive zu übergeben. Die Verbindung zwischen der Überkomponente und der Komponente geschieht im Template.

Interpolation: Ein Ausdruck in einem Template wird evaluiert und dann in ein String konvertiert. Der String wird dann mit benachbarten Strings konkateniert und in den DOM gesetzt. Um Angular zu sagen was interpoliert werden muss, wird der Ausdruck zwischen {{ und }} gesetzt.

Komponente: Komponenten sind die Haupt-UI-Bausteine einer Angular 2 Anwendung. Sie kombinieren Logik mit einem Angular-Template (View), um die Anwendung anzuzeigen. Zu einer Komponente gehören immer Metadaten für die Komponente (@Component) und eine Klasse (Komponenten-Klasse), die die Logik für die Komponente beinhaltet.

output-Eigenschaft: Ist eine Eigenschaft einer Komponente/Direktive, die die Komponente/Direktive nutzen kann, um Daten an die Überkomponente zu übergeben. Die Verbindung zwischen output-Eigenschaft und Überkomponente geschieht im Template. Genauer gesagt definieren output-Eigenschaften Events auf die eine Überkomponente hören und reagieren kann.

Pipes: Sind Funktionen die Eingabewerte transformieren, um diese anschließend in eine View anzuzeigen. Ähnliche Funktionen gab es auch in Angular 1.x. Da waren sie unter dem Namen “Filter” bekannt.

Service: Angular 2 Services sind im Grunde genommen TypeScript Klassen, die wir mittels Dependency Injection in unsere Anwendung verwenden können. Z. B. kann ein Service verschiedene Hilfsmethoden beinhalten.