Webanwendungen mit AngularJS
Seite 3: Filter und Kommunikation
Dies bewirken in AngularJS die sogenannten Services, die im Gegensatz zu den Controllern als Singleton instanziiert werden und anwendungsweit gültig sind. Sie entsprechen im MVC-Entwurfsmuster dem Model, enthalten also die Geschäftslogik und binden Ressourcen wie Webdienste oder Datenbanken an.
Um einen Service zu definieren, kennt AngularJS verschiedene Wege. Der am häufigsten verwendete hat die Funktion factory zur Grundlage, die per Definition ein Objekt zurückgibt, das die Funktionen des Services enthält. Da ein Service nicht an eine konkrete Ansicht gekoppelt ist, verfügt er über keinen Kontext:
app.factory('calculator', [ function () {
return {
add: function (first, second) {
var sum = first + second;
return sum;
}
};
}]);
Um einen Service in einem Controller zu verwenden, ist ihm der Service als Abhängigkeit hinzuzufügen. Ein einfacher Taschenrechner lässt sich also wie folgt implementieren:
<div ng-controller="CalculatorController">
<input ng-model="first" /> + <input ng-model="second" />
<button ng-click="add(first, second)">=</button>
<input ng-model="sum" />
</div>
app.controller('CalculatorController', [
'$scope', 'calculator', function ($scope, calculator) {
$scope.add = function () {
var first = $scope.first - 0,
second = $scope.second - 0;
$scope.sum = calculator.add(first, second);
};
}
]);
Das Auflösen der Abhängigkeiten führt AngularJS auf Grundlage der angegebenen Parameter durch. Sobald man den Code jedoch minifiziert, ändern sich die Namen der Parameter, beispielsweise zu den deutlich kürzeren Bezeichnern a und b. Damit AngularJS auch in diesem Fall die Abhängigkeiten korrekt auflösen kann, übergibt man sie zusätzlich als gesonderte Zeichenketten. Das erklärt die doppelte Angabe von $scope und calculator im Code.
Kommunikation ist alles
Für den Austausch zwischen Controllern und Services enthält AngularJS einen Kommunikationskanal. Jeder Kontext umfasst die beiden Funktionen $emit und $broadcast, mit denen sich Nachrichten versenden lassen. Auch die Übergabe von Parametern ist möglich, wie das folgende Beispiel zeigt:
$scope.$broadcast('foo', { foo: 'bar' });
$emit und $broadcast unterscheiden sich dabei in einem kleinen, aber wichtigen Detail: $emit sendet eine Nachricht an alle übergeordneten Kontexte, $broadcast an alle untergeordneten. Die Kommunikation funktioniert also zunächst nur für Controller, die ineinander verschachtelt sind – nicht jedoch für Controller auf der gleichen Ebene oder gar für Services (s. Abb. 2).
Abhilfe schafft die Verwendung des $rootScope-Kontexts: Da er, wie sein Name nahelegt, den obersten Kontext einer Anwendung darstellt, erreicht eine per $broadcast gesendete Nachricht jeden anderen Kontext. Auf diesem Weg kann man eine Nachricht daher an alle Komponenten einer Anwendung senden. Wichtig ist allerdings, darauf zu achten, dass man den $rootScope-Kontext als gesonderte Abhängigkeit angeben muss:
app.factory('calculator', [ '$rootScope', function ($rootScope) {
return {
add: function (first, second) {
var sum = first + second;
$rootScope.$broadcast('calculatedSum', { result: sum });
return sum;
}
};
}]);
Jeder Controller und Service kann nun auf diese Nachricht reagieren, indem er eine ereignisbehandelnde Funktion via $on in seinem Kontext registriert:
app.controller('FooController', [ '$scope', function ($scope) {
$scope.$on('calculatedSum', function (data) {
console.log(data.result);
});
}]);
Wie erwähnt sieht die Philosophie von AngularJS vor, niemals das DOM aus einem Controller heraus zu verändern. Der Einfluss des Entwicklers beschränkt sich auf die Kontexte der Controller, das Aktualisieren der Anzeige übernimmt die MVVM nutzende Datenbindung.
Zur Aktualisierung gibt es zahlreiche weitere Direktiven, unter anderem ng-class und ng-style zum Steuern von CSS, ng-show und ng-hide zum Anzeigen beziehungsweise Verbergen von Bereichen sowie ng-href und ng-src zum Verwalten von Verweisen und Datenquellen.
Darüber hinaus lassen sich eigene Direktiven schreiben, wofür zwei Syntaxvarianten zur Verfügung stehen: Die eine ermöglicht ausschließlich das Definieren eigener Attribute, die zweite hingegen zusätzlich das Erzeugen von Elementen, CSS-Klassen und Kommentaren. Der Vorteil der ersten Variante ist, dass man deutlich weniger Code schreiben muss, sodass man ihr in der Praxis häufig den Vorzug gibt.
Um beispielweise ein Attribut namens alert-text zu definieren, das bei einem Klick auf das zugeordnete Element eine Meldung ausgibt, genĂĽgt in der kĂĽrzeren Variante folgender Code:
app.directive('alertText', [ function () {
return function (scope, element, attributes) {
element.bind('click', function () {
alert(attributes.alertText);
});
};
}]);
Anschließend lässt sich die Direktive einem beliebigen HTML-Element hinzufügen:
<div alert-text="Hello world!">
// ...
</div>
Innerhalb der Direktive steht das Element über den Parameter element zur Verfügung. Er stellt die einzige Stelle in AngularJS dar, an der ein direkter Zugriff auf das DOM vorgesehen ist. Hierfür enthält AngularJS eine abgespeckte Version von jQuery, sodass man auf die wichtigsten Funktionen der Bibliothek zugreifen kann, ohne sie explizit einbinden zu müssen.
Der Parameter attributes ermöglicht die Einsicht auf sämtliche Attribute des HTML-Elements, was im Beispiel dazu verwendet wird, den anzuzeigenden Text zu ermitteln. Mit dem Parameter scope lässt sich auf den Kontext des zugehörigen Controllers zugreifen.
Verwendet man zur Definition von Direktiven statt der kürzeren die längere Variante, kann man weitaus mehr Einfluss nehmen. Insbesondere die Möglichkeit, einen eigenen Kontext für die Direktive zu erzeugen und sie teilweise mit dem übergeordneten Kontext eines Controllers verbinden zu können, ist hilfreich.