AngularJS

Information

Diese kurze AngularJS-Einführung wurde von mir im Zuge meiner ersten Schritte mit dem AngularJS-Framework erstellt. Ich habe beim Lernen von AngularJS das Buch "AngularJS - Eine praktische Einführung in das Javascript-Framework" von Phillip Tarasiewicz und Robin Böhm, erschienen im dpunkt.verlag gelesen. Ich kann das Buch wämstens weiterempfehlen.

Besondere Merkmale

  • Dependency Injection
  • Two-Way Data-Binding
  • Änderungen im Datenmodell wirken sich automatisch auf die Elemente in der Ansicht aus und umgekehrt (vermeidet Boilerplate-Code).
  • Benutzerdefinierte HTML-Elemente und -Attribute über Direktiven.
  • Gute Testunterstützung inkl. Mocks

Module

Module sind das gröbste Strukturmittel in AngularJS und kapseln eine Menge zusammenhängender Anwendungskomponenten. Sie dienen u.a. als Container für…

  • Controller
  • Services
  • Direktiven

AngularJS-Module sollen die folgenden Eigenschaften besitzen:

  • hohe innere Kohäsion
  • klar definiertes API
  • hohes Maß an Wiederverwendbarkeit

Mit der ngApp-Directive wird AngularJS mitgeteilt, welches der Module das Startmodul ist. Dieses wird automatisch initialisiert und ausgeführt, wenn das DOM der Anwendung vollständig geladen ist.

Controller

Controller sind Konstruktionsvorschriften für Scopes und definieren die Daten und Logik, die für eine bestimmte Ansicht benötigt werden. Jeder Aufruf eines Controller bekommt einen eigenen Scope.

Controller haben eine enge Bindung zu Services. Die Bereitstellung dieser Services geschieht über Dependency Injection. In Folge dessen können beim Testen bestimmte Services durch Mock-Implementierungen ausgetauscht werden.

Konstruktionsvorschriften für Controller

  • Controller sollten schmal designet werden
  • Der Quellcode sollte aussagekräftig sein
  • Komplexe Logik gehört in die Services
  • Controller halten keine Daten, sondern sollten in Ihren Scopes nur Referenzen auf Daten bereitstellen.
  • Controller nehmen keinen Einfluss auf das DOM. Statt dessen sollten möglichst die Standarddirektiven wie ngRepeat, ngShow oder ngClass verwendet werden. Alternativ ist eine eigene Direktive zu erstellen, die die Manipulation kapselt.

Models

Models sind die datenhaltenden Objekte in AngularJS. Diese sind normale Datentypen und müssen nicht von besonderen Framework-Klassen erben. Es können beispielsweise einfache Objekte, Arrays oder primitive Datentypen als Model verwendet werden.

Ein Model muss in einem Scope definiert sein, damit die Zwei-Wege-Datenbindung funktioniert.

Routen

Routen dienen der Adressierung einzelner Anwendungsteile über eine URL und der Unterstützung der Vorwärts- bzw. Rückwärtsfunktionalität des Browsers (Deep Linking). Das Ermöglicht die Abbildung von URLs auf Templates mit zugehörigen Controllern.

Der Standardmodul für routing ist ngRoute. Es ermöglicht die Zuweisung von Routen jeweils zu einem Template und einem Controller.

angular.module('myApp').config(function ($routeProvider) {
  $routeProvider
    .when('/', {
      templateUrl: 'templates/mainTemplate.html',
      controller: 'MainCtrl'
    })
    .when('/user/:userId', {
      templateUrl: 'templates/userDetailsTemplate.html',
      controller: 'UserDetailsCtrl'
    })
    .otherwise({
      redirectTo: '/'
    });
});

Wenn man beispielsweise innerhalb einer Ansicht weitere Ansichten abhängig von der Route verschachteln will, so stößt man schnell an die Grenzen des ngRoute-Moduls. Eine beliebte Alternative ist UI-Router.

Es ist eine neue Version des Standardroute-Moduls geplant, das die Vorteile beider Varianten vereinen soll.

Views

Eine View ist das Ergebnis aller zur Zeit instanziierten Templates. Sie entsteht zur Laufzeit, wenn AngularJS die Templates für den Anwendungsteil kompiliert und die Direktiven und Expressions durch konkrete Werte, wie zum Beispiel DOM-Knoten ersetzt hat.

Templates

Templates sind HTML-Fragmente die einen Teil des HTML-Codes der Anwendung darstellen. Die Templates dürfen in AngularJS Direktiven und Expressions enthalten.

Expressions

Expressions werden in erster Linie verwendet, um in einem Template auf Daten zuzugreifen, die in einem Scope definiert wurden. Expression werden innerhalb eines Templates in doppelten geschweiften Klammern notiert.

<div>
  <p>Hello, {{user.name}}</p>
</div>

Besonderheiten von Expressions:

  • Die Auswertung erfolgt gegen den Scope, der im Kontext des Templates in welchem die Expression verwendet wird, gültig ist.
  • Expressions die zu undefined oder null evaluieren, produzieren keine Ausgabe.
  • Kontrollfluss-Anweisungen wie Fallunterscheidungen oder Schleifen können nicht verwendet werden.

Innerhalb von Expressions können Filter verwendet werden, die in den folgenden Kapiteln beschrieben werden.

Formatierungsfilter

Formatierungsfilter ermöglichen es, den innerhalb einer Expression evaluierten Ausdruck, nachträglich zu formatieren, zu filtern oder zu transformieren.

Das folgende Beispiel zeigt die Nutzung des Filters uppercase. Er bewirkt, dass der Nutzername in Großbuchstaben ausgegeben wird.

<div>
  <p>Hallo, {{user.name | uppercase}}</p>
</div>

Es können auch eigene Filter, wie folgt skizziert, erstellt werden.

angular.module('myApp').filter('filterName', function() {
  return function(input) {
    var output = '';
    ...
    return output;
  };
});

Collection-Filter

Collection-Filter werden innerhalb der Directive ng-repeat verwendet.

Das folgende Beispiel gibt alle Familienmitglieder eines Benutzers, unter Zuhilfenahme der ng-repeat-Direktive aus.

<div>
  Familienmitglieder von {{user.name}}
  <ul>
    <li ng-repeat='member in family'>{{member}}</li>
  </ul>
</div>

Das oben skizzierte Beispiel iteriert über die Scope-Variable family, die ein Array mit Zeichenketten enthält und generiert für jeden Eintrag des Arrays einen Listeneintrag, in welchem die jeweilige Zeichenkette ausgegeben wird.

Die Listeneinträge können mit dem Collection-Filter Namens ‚filter' - wie im folgenden Beispiel skizziert - reduziert werden.

<div>
  Familienmitglieder von {{user.name}}
  <ul>
    <li ng-repeat='member in family'>{{member | filter: 'Bastian'}}</li>
  </ul>
</div>

Im Beispiel wird durch die Notation filter: 'Bastian' erreicht, dass ausschließlich Familienmitglieder ausgegeben werden, die den String ‚Bastian' enthalten. Zwischen Groß- und Kleinschreibung wird hierbei nicht unterschieden.

Weitere Collection-Filter des Standard-API sind u.a. limitTo und orderBy.

Eigene Collection-Filter

Es können auch eigene Filter, wie folgt, erstellt werden.

angular.module('myApp').filter('endsWith'), function() {
  return function(inputArray, endsWith) {
    var outputArray = [];

    angular.forEach(inputArray, function(item) {
      if(item.length >= endsWith.length &&
        item.substring(item.length – endsWith.length)  === endsWidth) {
        outputArray.push(item);
      }
    });
    return outputArray;
  };
});

Die folgende Anwendung des Beispielfilters gibt ausschließlich Namen aus, die auf ‚Nolte' enden.

<div>
  Familienmitglieder von {{user.name}}
  <ul>
    <li ng-repeat="member in family | endsWith:'Nolte'">{{member}}</li>
  </ul>
</div>

Services

Services werden im Kontext mit AngularJS für eine Reihe von Aufgaben verwendet. Komplexere Routinen und Algorithmen werden üblicherweise in Services ausgelagert.

Insbesondere können Services in anderen Services verwendet werden. Hierbei kommt Dependency Injection zum Einsatz.

Jeder Service wird innerhalb einer Anwendung genau einmal instanziiert (Singleton Pattern).

Der Datenzugriff sollte in AngularJS immer über Services realisiert werden.

AngularJS bietet 5 Varianten für die Definition von Services die in der folgenden Tabelle dargestellt werden.

Servicedefinition

API

Beschreibung

Provider

provider(…)

Grundlegendes API zur Servicedefinition. Kann in der Konfigurationsphase eingestellt werden. Muss eine Funktion $get() bereitstellen, die eine Serviceinstanz zurückgibt. Kann mit einer Konstruktorfunktion oder mit einem Objekt aufgerufen werden.

Factory

factory(…)

Baut auf dem Provider-API auf. Abstrahiert von dem Aspekt der Konfigurierbarkeit. Erlaubt bequeme Servicedefinition, sofern keine Konfigurierbarkeit erforderlich ist.

Konstruktor- funktion

service(…)

Baut auf dem Factory-API auf. Instanziierung der Serviceinstanz mithilfe des new-Operators in Verbindung mit der übergreifenden Konstruktorfunktion. Somit lässt sich ein Objekt einer bestehenden Klasse als Service registrieren.

Wert als Service

value(…)

Baut auf dem Factory-API auf. Erlaubt die Registrierung eines Wertes als Service. Der Wert kann ein primitiver Datentyp, ein Objekt oder eine Funktion sein.

Konstante als Service

constant(…)

Registrierung eines konstanten Wertes als Service. Dieser Wert kann zur Laufzeit nicht mehr verändert werden.

Mit der Provider-Funktion greift man direkt auf den providerCache zu und kann auf diese Weise einen Provider registrieren.

Die Singleton-Instanzen der Services werden im instanceCache verwaltet. Verwendet man die Provider-Funktion, so kann das Serviceobjekt in der Konfigurationsphase konfiguriert werden. Diese Möglichkeit steht bei den anderen Servicevarianten nicht zur Verfügung.

Eine Service-Factory nutzt intern die provider()-Funktion und vereinfacht die Definition. Der Nachteil dieser Methode ist, dass der Service nicht konfiguriert werden kann.

Weitere Informationen im Buch „AngularJS“ auf Seite 42 und folgenden.

Rudimentäres Beispiel eines Log-Service.

angular.module('myApp').provider('log', function() {
  // Private Variablen/Methoden
  var prefix = '',
  suffix = '';

	// Serviceimplementierung (private)
	var log = function(level, message) {
		console.log(prexfix + '[', + level + '] ' + message + suffix);
	};

	// Öffentliche Variablen/Methoden (this...)
	// Konfigurations-Methoden
	this.setPrefix = function(prefix2set) {
		prefix = prefix2set;
	};

	this.setSuffix = function(suffix2set) {
		suffix = suffix2set;
	};

	this.$get() = function() {
		// Public API
		error: function(message) {
			log('ERROR', message);
		},
		info: function(message) {
			log('INFO', message);
		},
		debug: function(message) {
			log('DEBUG', message);
		}
	};
});

Die Methoden setPrefix() und setSuffix() sind öffentlich und können während der Konfigurationsphase in einem config()-Block aufgerufen werden, um den Service zu konfigurieren. Die $get()-Methode sollte nie direkt aus dem Applikationscode aufgerufen werden. Diese wird vielmehr von AngularJS für die Erstellung der Singleton-Instanz verwendet.

Das folgende Beispiel zeigt exemplarisch die Konfiguration des Provider-Services. Der Provider wird der Konfigurationsfunktion automatisch via Dependency Injection übergeben. Damit dies funktioniert, ist dem Servicenamen das Wort Provider anzuhängen.

angular.module('myApp').config(function (logProvider) {
  logProvider.setPrefix('[myApp] ');
  logProvider.setSuffix('.');
});

Um einen Service innerhalb eines Controllers zu verwenden, genügt es seinen Namen als Parameter in der Controller-Funktion zu spezifizieren.

angular.module('myApp').controller('MyCtrl', function($scope, log) {
  log.debug('MyCtrl wurde aufgerufen');
});

Direktiven

Direktiven stellen ein Alleinstellungsmerkmal von AngularJS dar. Direktiven erweitern das Vokabular von HTML. Mit Direktiven können neue HTML-Tags und Attribute erstellt werden. Sie sind ein mächtiges Feature, das die Erstellung von strukturiertem und wiederverwendbarem Anwendungscode ermöglicht.

Direktiven lassen sich mit der directive()-Funktion definieren. Diese erwartet zwei Parameter, den Direktivennamen und eine Factory-Funktion. Ein Direktivenname ist in den CamelCase-Schreibweise, mit einem Kleinbuchstaben beginnend – z.B. colorPicker - zu notieren. Wird die Direktive als HTML-Tag oder –Attribut verwendet, so ist die SnakeCase-Notation – z.B. color-picker - zu verwenden.

Mögliche Varianten der Notation sind hierbei:

  • color-picker
  • color:picker
  • data-color-picker (Kompatibilität mit älteren Browsern)
  • x-color-picker (Kompatibilität mit älteren Browsern)

Die Link-Funktion stellt die einfachere Variante der Direktivendefinition dar und ist für einfache Fälle, ohne das Erfordernis der fein granulierten Steuerung von Eigenschaften, gedacht.

angular.modul('myApp').directive('colorPicker', function() {
  return function(scope, element, attrs) {
    console.log('Ich bin es, der Color-Picker!') ;
  }
});

Die Link-Funktion wird für jede Instanz der Direktive genau einmal aufgerufen. Dies ist eine gute Stelle um beispielsweise Event-Handler zu registrieren. Der Funktion werden von AngularJS mindestens 3 Parameter übergeben.

Parameter

Beschreibung

scope

Eine Referenz auf den Scope der für die Direktive gültig ist.

element

Eine Referenz auf das DOM-Element, auf welches diese Direktive wirken soll.

attrs

Eine Referenz mit der auf die HTML-Attribute des DOM-Elementes zugegriffen werden kann.

<div color-picker></div>

Definition über Direktiven-Definitions-Objekt

Die Definition einer Direktive über ein Direktiven-Definitions-Objekt erlaubt die feingranulierte Beschreibung des Verhaltens der Direktive.

colorPicherApp.directive('colorPicker', function() {
  return {
    restrict: 'E',
    link: function(scope, element, attrs) {
      console.log('Ich bin es der Color-Picker!');
    }
  };
});

Über die restrict-Eigenschaft besteht die Möglichkeit dem Direktiven-Definitions-Objekt einen Geltungsbereich festzulegen.

<color-picker></color-picker>

Restrict-Eigenschaft

Die Restrict-Eigenschaft legt fest, auf welche HTML-Fragmente sich eine Directive auswirken soll. Die Eigenschaft akzeptiert einen, oder auch eine Kombination der folgenden Buchstaben.

Buchstabe

Beschreibung

E

HTML-Element (tag)

A

HTML-Attribut

C

CSS-Klasse

M

HTML-Kommentar

Scope-Eigenschaft

Über die Scope-Eigenschaft können Sichetbarkeitsregeln für Variablen festgelegt werden. Wird kein Scope festgelegt, so wird der Root-Scope verwendet. Dies sollte vermieden werden, um Seiteneffekte zu verhindern. Wird kein Scope gesetzt, so teilen sich alle Instanzen die selben Variablen, was letzendlich dazu führt, dass die Manipulation an den Variablen einer Direktiven-Instanz sich auch auf alle anderen Instanzen auswirkt.

Wird die Scope-Eigenschaft auf 'true' gesetzt, so erhält jede Instanz der Direktive einen eigenen Scope.

colorPicherApp.directive('colorPicker', function() {
  return {
    scope: true,
    templateUrl: 'templates/colorPickerTemplate.html',
    restrict: 'E',
    link: function(scope, element, attrs) {
      console.log('Ich bin es der Color-Picker!');
    }
  };
});

Der Scope erbt allerdings prototypisch von seinem Elternscope (z.B. dem Root-Scope). Somit können Variablen des Eltern-Scope immernoch aus der Direktive heraus manipuliert werden. Um dies zu vermeiden, bietet sich der Einsatz isolierter Scopes an.

Isolierte Scopes

Ein isolierter Scope ist zwar Teil der Scope-Hierarchie, erbt aber nicht prototypisch von seinem Eltern-Scope. Dies verhindert die versehentliche Manipulation des Eltern-Scopes und vermeidet, dass man Abhängigkeiten zum übergeordnenten Scope eingeht. Isolierte Scopes sind ein Schüsselelement bei der Erstellung wiederverwendbarer in sich geschlossener Komponenten. Der einfachste Weg einen isolierten Scope zu erzeugen, ist der Scope-Eigenschaft ein leeres Objekt zuzuweisen.

colorPicherApp.directive('colorPicker', function() {
  return {
    scope: {},
    templateUrl: 'templates/colorPickerTemplate.html',
    restrict: 'E',
    link: function(scope, element, attrs) {
      console.log('Ich bin es der Color-Picker!');
    }
  };
});

Nur in den seltensten Fällen möchte man eine vollständig in sich geschlossene Komponente erzeugen. AngularJS ermöglicht es daher kontrollierte Verbindungen zum umgebenden Kontext aufzubauen.

Hierbei gibt es 3 Ausprägungen, die durch Angabe von Präfixen definiert werden:

Typ

Präfix

Beispiel

Beschreibung

Unidirektional

@

scope: { red: '@initR' }

Einseite Verbindung vom HTML-Attribut des umgebenden Kontexts zur Direktive. Diese wird verwendet, wenn die Direktive über Änderungen der Variablen im umgebenden Kontekt informiert werden, aber selbst keine Änderungen an diesen Variablen vernehmen soll. Wird auch zum Setzen von Initialwerten verwendet.

Bidirektional

=

scope: { red: '=r' }

Beidseitige Verbindung zwischen Direktive und HTML-Attribut des umgebenden Kontexts. Dies hilft dabei Boilerplate-Code zu vermeiden.

Funktionsaufruf

&

scope: { onChange: '&funct1' }

Referenz auf eine Funktion des umgebenden Kontextes, auf die im benannten HTML-Attribut referenziert wird.

Die Zeichenkette, die dem Präfix folgt, spezifiziert, welches HTML-Attribut die Information enthält, welche Variable, bzw. Funktion aus dem Eletern-Scope an der Verbindung beteiligt sein soll. Sind der Name der Variable der Direktive und der Name des Attributes identisch, so genügt es ausschließlich den Präfix anzugeben.

Template-URL-Eigenschaft

Die Eigenschaft templateUrl dient der Referenzierung des zur Direktive zugehörigen Templates.

colorPicherApp.directive('colorPicker', function() {
  return {
    scope: true,
    templateUrl: 'templates/colorPickerTemplate.html',
    restrict: 'E',
    link: function(scope, element, attrs) {
      console.log('Ich bin's der Color-Picker!');
    }
  };
});