AngularJS ist ein schmales Javascript Framework, dass sich Perfekt für Single-Page Applikationen eignet, die mit einem REST Service sprechen. Ich persönlich nutze zum Erstellen einer AngularJS Applikation gerne Yeoman. Mit Hilfe der Generatoren können schnell verschiedenste Javascript Applikationen erstellt werden. Ich werde in diesem Beitrag nicht weiter darauf eingehen wie die Anwendung erstellt wird und hoffe, dass die Erklärung auf der Yeoman Seite ausreichend ist.
Dieser Artikel ist aufbauend auf REST mit Play 2 und ReactiveMongo und nutzt die dort implementierte REST Schnittstelle.
Konfiguration und Routen
Zu Beginn unseres Projekts müssen wir einige kleine Konfigurationsaufgaben übernehmen. Einerseits wollen wir einen Service nutzen, der nicht unter der gleichen URL zu erreichen ist, wie unsere Applikation. Damit moderne Browser dies Akzeptieren und es nicht wegen der Cross-Origin-Policy unterbinden müssen wir useXDomain auf true setzen und X-Request-With aus unserem Header entfernen. Zusätzlich legen wir noch unsere Routen an, die sagen bei welcher URL, welcher Controller und welche View genutzt werden soll.
angular .module('angularJsexampleApp', [ 'ngAnimate', 'ngCookies', 'ngResource', 'ngRoute', 'ngSanitize', 'ngTouch' ]) .config(function ($routeProvider, $httpProvider) { $httpProvider.defaults.useXDomain = true; delete $httpProvider.defaults.headers.common['X-Requested-With']; $routeProvider .when('/', { templateUrl: 'views/main.html', controller: 'MainCtrl' }) .when('/products', { templateUrl: 'views/products/list.html', controller: 'ProductListCtrl' }) .when('/products/create', { templateUrl: 'views/products/create.html', controller: 'ProductCreateCtrl' }) .when('/products/edit/:id', { templateUrl: 'views/products/edit.html', controller: 'ProductEditCtrl' }) .when('/products/delete/:id', { templateUrl: 'views/products/list.html', controller: 'ProductDeleteCtrl' }) .when('/products/:id', { templateUrl: 'views/products/detail.html', controller: 'ProductDetailCtrl' }) .otherwise({ redirectTo: '/' }); });
Services
AngularJS bietet verschiedene, sehr gute Module an, die einem beim Entwickeln viel Zeit sparen. Eine dieser Erweiterungen ist ng-resource. ng-resource erweitert den in AngularJS vorhandenen http Service, sodass der REST Spezifikation entsprechende Schnittstellen ohne viel Aufwand genutzt werden können, da die Grundoperationen wie etwa create, list, get, delete und update bereits implementiert sind.
Um die Schnittstelle ansprechen zu können, erstellen wir uns eine Factory und verweisen auf unsere Resource:
angular.module('angularJsexampleApp') .factory('Product', ['Config', '$resource', function (Config, $resource) { return $resource(Config.service + '/products/:id', {id: '@id'}, { update: { method: 'PUT' } }); }]);
Damit wir bei der Update Methode anstelle von POST mit dem HTTP-Request Methode PUT arbeiten können, müssen wir dies unserer Resource mitteilen. Andernfalls wird von AngularJS POST beim Update genutzt.
Ich habe mich dazu entschieden, die URL der Resource in eine Config Datei auszulagern. Diese könnte ich je nachdem in welcher Umgebung ich mich befinde (Lokal, Test, Produktion) editieren oder austauschen. Die Konfigurationsdatei ist ebenfalls wie unsere Resource Factory ein Service. Allerdings diesmal nicht als Factory, sondern als Constant.
angular.module('angularJsexampleApp') .constant('Config', {'service': 'http://localhost:8080'});
Controller
Wir haben verschiedene Präsentationsschichten in unsere Applikation, um Produkte anzuzeigen, zu erstellen, zu ändern oder zu löschen. Für jede Präsentationsschicht erstellen wir einen eigenen Controller, der die Steuerung zwischen Präsentation und Model übernimmt.
Zusätzlich zu unseren produktspezifischen Controllern haben wir noch den Shoppingcart- & Wishlistcontroller. Hiermit will ich das Controller übergreifende nutzen von einem Model mit Hilfe eines weiteren Service zeigen.
Produktcontroller
Detail
Der DetailController nutzt die GET Methode unserer Resource um einzlene Produkte anzuzeigen. Zusätzlich haben wir noch eine Methode die es uns ermöglicht einen Kommentar zu unserem Produkt zu erstellen. Dabei wird ein UPDATE auf unsere Resource gemacht.
Da unser Service MongoDB nutzt um die Daten zu speichern, müssen wir die ID des Objekts bei einem Update auf undefined setzen. Andernfalls können wir keine Änderung durchführen.
angular.module('angularJsexampleApp') .controller('ProductDetailCtrl', ['$scope', '$routeParams', 'Product', function ($scope, $routeParams, Product) { $scope.product = Product.get({id: $routeParams.id}); $scope.addComment = function () { /* * drop mongodb _id else it's not possible to update the product */ $scope.product._id = undefined; if ($scope.product.comments == undefined) { $scope.product.comments = []; } $scope.product.comments.push($scope.comment); $scope.product.$update({id: $routeParams.id}); } }]);
List
Der ListController holt von unserer Resource die Liste aller im System existierenden Produkte. Zusätzlich nutzt er den Wishlist- und Shoppingcartservice um Produkte unserem Einkaufskorb oder der Wunschliste hinzuzufügen. Hier sieht man, dass sowohl von unserem Listcontroller, als auch von unserem Shoppingcart- oder Wishlistcontroller die gleiche Liste genutzt wird.
angular.module('angularJsexampleApp') .controller('ProductListCtrl', ['$scope', 'Product', 'Shoppingcart', 'Wishlist', function ($scope, Product, Shoppingcart, Wishlist) { $scope.products = Product.query(); $scope.shoppingcart = Shoppingcart.items; $scope.wishlist = Wishlist.items; $scope.addToWishlist = function(product) { Wishlist.add(product); }; $scope.addToCart = function(product) { Shoppingcart.add(product); }; }]);
Create
Der CreateController dient zum Erstellen eines neuen Produkts. Sobald das Produkt erstellt ist, wollen wir auf die Listenübersicht aller Produkte wechseln.
angular.module('angularJsexampleApp') .controller('ProductCreateCtrl', ['$scope', '$location', 'Product', function ($scope, $location, Product) { $scope.product = new Product; $scope.save = function () { $scope.product.$save(function () { $location.path('/products'); }); } }]);
Update
Hier können wir ein Produkt ändern oder aktualisieren. Wieder müssen wir die ID des Objekts auf undefined setzen, damit das Update funktioniert.
angular.module('angularJsexampleApp') .controller('ProductEditCtrl', ['$scope', '$routeParams', '$location', 'Product', function ($scope, $routeParams, $location, Product) { $scope.product = Product.get({id: $routeParams.id}); $scope.edit = function () { /* * drop mongodb _id else it's not possible to update the product */ $scope.product._id = undefined; $scope.product.$update({id: $routeParams.id}, function () { $location.path('/products'); }); } }]);
Delete
Der Deletecontroller übernimmt das Löschen eines Produkts. Ebenso wie beim Erstellen wollen wir wieder auf der Liste aller Produkte landen.
angular.module('angularJsexampleApp') .controller('ProductDeleteCtrl', ['$scope', '$routeParams', '$location', 'Product', function ($scope, $routeParams, $location, Product) { $scope.product = Product.delete({id: $routeParams.id}, function () { $location.path('/products'); }); }]);
Wishlistcontroller
Unser Wishlistcontroller ist denkbar einfach. Er konsumiert den Wishlist Service und speichert die in der Wunschliste vorhandenen Produkte im Scope, sodass man in der View darauf zugreifen kann. Zusätzlich bietet er eine Möglichkeit Produkte aus der Wunschliste zu entfernen.
angular.module('angularJsexampleApp') .controller('WishlistCtrl', ['Wishlist', '$scope', function (Wishlist, $scope) { $scope.wishlist = Wishlist.items; $scope.removeItem = function(product) { Wishlist.remove(product); }; }]);
Wishlistservice
Damit die Wunschliste controllerübergreifend genutzt werden kann muss sie als Service bereitgestellt werden. Somit können wir uns sicher sein, dass jeder Controller die gleiche Instanz der Wunschliste nutzt. Die Implementierung ist relativ simpel. Wir haben die Möglichkeit ein Produkt hinzuzufügen, zu entfernen und können Überprüfen ob das Produkt bereits in der Liste vorhanden ist.
angular.module('angularJsexampleApp') .service('Wishlist', function() { return { items: [], add: function (product) { if(!this.contains(product)) { this.items.push(product); } }, contains: function(product) { for (var i = 0; i < this.items.length; i++) { if (this.items[i]._id === product._id) { return true; } } }, remove: function(product) { this.items.splice(this.items.indexOf(product), 1); } }; });
Auf die Views werde ich hier nicht weiter eingehen. Die AngularJS Keywords sind sehr gut in der offiziellen Dokumentation erklärt. Der Code ist auf GitHub zu finden. Eine Demo Applikation auf Heroku. Da auch der REST Service auf Heroku gehostet wurde, kann es einen Moment dauern bis die ersten Ergebnisse von der Schnittstelle geliefert werden. Dies hängt damit zusammen, dass Heroku die Applikation herunterfährt wenn sie einige Zeit nicht genutzt wurde.