senäh

17senäh und so…

mean_thumb

HTML/CSS/JS
03. Nov 2013
Kommentare: 5

Restangular

Kategorien: HTML/CSS/JS | 03. Nov 2013 | Kommentare: 5

Serie: Einführung in den MEAN Stack

Nachdem wir beim letzten Mal AngularJS nur oberflächlich betrachten konnten, will ich euch heute vorstellen, wie ihr mit AngularJS eine REST API konsumieren könnt.

Zu diesem Zweck verwenden wir die REST API, die wir im Artikel über Baucis erstellt haben. Wir fügen dem Beispiel in der vorletzten Zeile lediglich einen statischen Dateiserver hinzu, damit wir HTML, CSS und JavaScript ausgeben können:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var mongoose = require('mongoose');
var baucis = require('baucis');
var express = require('express');

mongoose.connect('mongodb://localhost:27017/todo-db');

var TodoSchema = new mongoose.Schema({
    title: { type: String, default: '' },
    completed: { type: Boolean, default: false }
});
mongoose.model('todo', TodoSchema);

baucis.rest({
  singular: 'todo',
  plural: 'todos'
});

express()
  .use('/api', baucis())
  .use(express.static(__dirname))  // new line
  .listen(1337, '127.0.0.1');

Denkt daran vorher Mongoose, Baucis und Express zu installieren sowie den Node-Server und MongoDB zu starten. Auch AngularJS solltet ihr wieder mit Bower installieren.

Nach dieser Vorbereitung können wir nun mit dem Beispiel beginnen. Wie Node ist auch AngularJS in verschiedene Module aufgeteilt. Eines davon ist $http, mit welchem ihr HTTP-Anfragen durchführen könnt und welches zu den Core Modulen von AngularJS zählt. D.h., es wird automatisch mit AngularJS mitinstalliert. Ein anderes Modul mit dem ihr HTTP-Anfragen durchführen könnt ist $resource. Es setzt auf $http auf und ist direkt für die Verwendung mit REST APIs gedacht. Es wird ebenfalls vom AngularJS-Team entwickelt, muss jedoch zusätzlich installiert werden. Ich möchte euch heute jedoch ein anderes AngularJS-Modul als $resource vorstellen: Restangular. Restangular ist $resource sehr ähnlich, bietet aber noch mehr Komfort und Flexibilität. Es wird nicht vom AngularJS-Team, sondern von Martin Gontovnikas entwickelt. Ihr installiert es mit folgendem Befehl:

1
$ bower install restangular

Wenn ihr den Terminal aufmerksam beobachtet habt oder in euren bower_components Ordner schaut, werdet ihr feststellen, dass ihr neben Restangular noch ein weiteres Framework namens Lo-Dash heruntergeladen habt. Hier sehen wir einen der Gründe, warum wir Bower einsetzen: Manchmal hängen verschiedene Frameworks (bzw. Module/Komponenten) voneinander ab. Durch Package Manager wie Bower und npm müssen wir uns nicht um diese Abhängigkeiten kümmern und sparen Arbeit. Ich werde euch Lo-Dash an dieser Stelle nicht weiter erklären, da wir es nicht benutzen werden, aber es ist ein sehr nützliches kleines Framework, welches die Verwendung von Objekten, Arrays, Strings und ähnlichem vereinfacht. Ihr dürft gerne in die Dokumentation von Lo-Dash  hineinschnuppern, um mehr zu erfahren.

Möglicherweise raucht euch der Kopf bei den vielen Frameworks und ihr seht es als schlechte Praxis an zu viele Frameworks zu verwenden und zu installieren. Etwas Sorge ist natürlich angebracht, aber es handelt sich hierbei um wirklich nützliche und teilweise sehr kleine Frameworks. AngularJS mag zunächst ein sehr großes Framework sein, aber auf der anderen Seite reduziert es euren eigenen selbstgeschriebenen Code erheblich. Sollte euch die Dateigröße der Frameworks stören, dann sei euch gesagt, dass die größten Dateigrößenprobleme immer noch von Bildern auf Webseiten verursacht werden. So lange ihr eure Bilder auch nicht gründlich optimiert und komprimiert, solltet ihr euch noch keine (oder wenige) Gedanken über JavaScript-Dateien machen. Das ihr später all euren Code minifiziert und in eine Datei verpackt – davon gehe ich jetzt einmal aus ;)

Nachdem wir nun alle benötigten Ressourcen für unsere Webapp haben zeige ich euch in einem Rutsch unseren Client, den ich der besseren Lesbarkeit geschuldet komplett in einer einzelnen index.html verfasst habe:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<!doctype html>
<html ng-app="todoApp">
  <head>

    <!-- load scripts -->
    <script src="./bower_components/angular/angular.js"></script>
    <script src="./bower_components/lodash/dist/lodash.js"></script>
    <script src="./bower_components/restangular/dist/restangular.js"></script>

    <!-- create angular app -->
    <script>
      angular
        .module('todoApp', ['restangular'])
        .config(function(RestangularProvider) {
          RestangularProvider.setRestangularFields({
            id: '_id'
          });
        })
        .controller('TodoController', function($scope, Restangular) {

          $scope.todos = [];
          Restangular
            .all('api/todos')
            .getList()
            .then(function(todos) {
              $scope.todos = todos;
            });

          $scope.create = function() {
            $scope.todos
              .post({ title: $scope.newTodo })
              .then(function(todo){
                $scope.todos.push(todo);
              });
          };

          $scope.delete = function(todo, index) {
            todo
              .remove()
              .then(function() {
                $scope.todos.splice(index, 1);
              });
          };

        });
    </script>

    <!-- add style for completed todos -->
    <style>
      .completed {
        color: green;
      }
    </style>
  </head>
  <body>

    <!-- create view: first a form to create new todos, then show a list of existing todos -->
    <div ng-controller="TodoController">
      <form>
        New Todo:
        <input type="text" ng-model="newTodo">
        <button ng-click="create()">Add</button>
      </form>

      <ul>
        <li ng-repeat="todo in todos" ng-class="{ completed: todo.completed }">
          {{todo.title}}
          <button ng-click="delete(todo, $index)">Remove</button> or mark as completed:
          <input type="checkbox" ng-model="todo.completed" ng-change="todo.put()">
        </li>
      </ul>
    </div>

  </body>
</html>

Gehen wir den Code Schritt für Schritt durch. Im html-Element haben wir wie im letzten Artikel das Attribut ng-app verwendet. Diesen haben wir dieses Mal zusätzlich den Wert todoApp zugewiesen. Wenn wir zusätzliche Module wie Restangular verwenden wollen, dann brauchen wir eine Möglichkeit ein Modul direkt einer AngularJS-App zuzuweisen. Dies erfolgt zu einem späteren Zeitpunkt mit dem String todoApp.

Im nächsten Abschnitt binden wir AngularJS, Lo-Dash und Restangular ein. Soweit nichts Unbekanntes. Im produktiven Einsatz würde ich die Scripte ans Ende des HTML Dokuments setzen, aber aus Erklärungsgründen habe ich die gesamte Logik nach vorne geholt. Nun beginnt nämlich der spannende Abschnitt.

Mit folgenden Codezeilen erstellen wir eine AngularJS-App namens todoApp und sagen ihr, dass sie von Restangular abhängt:

1
2
      angular
        .module('todoApp', ['restangular'])

Anschließend folgt ein Konfigurationabschnitt, in welchem einzelne Module vor ihrer Benutzung konfiguriert werden können. In diesem Fall verändern wir eine Einstellung bei Restangular. So geht Restangular davon aus, dass die Daten der REST API pro Dokument jeweils ein id-Feld besitzen. Wie wir wissen, heißt dieses Feld bei MongoDB jedoch _id. Also müssen wir diese Einstellung ändern. Wir sehen an dieser Stelle auch das erste Mal die Dependency Injection. RestangularProvider muss RestangularProvider heißen! Heißt der Parameter anders, erhalten wir nicht das RestangularProvider-Objekt! Bei AngularJS sind die Namen von Parametern von Bedeutung!

1
2
3
4
5
        .config(function(RestangularProvider) {
          RestangularProvider.setRestangularFields({
            id: '_id'
          });
        })

Anschließend erstellen wir einen Controller. Der Controller erhält einen Namen, sodass er später einer View zugewiesen werden kann. Außerdem injecten wir ein $scope- und ein Restangular-Objekt. Hier ist wieder die Dependency Injection im Einsatz. Schreiben wir die Parameter mit einem anderen Namen, so erhalten wir nicht unsere gewünschten Objekte. Außerdem ist die Reihenfolge der Parameter egal. Ob wir erst $scope oder erst Restangular schreiben ist irrelevant. Restangular erleichtert uns die Verwendung unserer REST API. Was ist aber  $scope? $scope ist das Bindeglied zwischen View und Controller, welches two-way data binding ermöglicht. Alle Daten, die dem $scope-Objekt zugewiesen werden, sind im Controller und der View zugänglich. Mit anderen Worten: Das $scope-Objekt hält unser Model.

1
        .controller('TodoController', function($scope, Restangular) {

Wir wissen nun, dass das $scope-Objekt unser Model erhält und das wir die Daten unseres Models mit Restangular über unsere REST API erhalten. Diese Erkenntnis wird im nächsten Schritt umgesetzt. Zuerst benennen wir unser Model als todos-Feld auf dem $scope-Objekt. Es ist zu Beginn ein leeres Array. Das Array befüllen wir nun mit echten Daten. Wir sagen Restangular die URL zu unserer REST API und der Collection (/api/todos) und fordern anschließend mit getList() alle vorhandenen Todos an. Anschließend seht ihr die Funktion then(), welche eine Besonderheit darstellt. Das Objekt, welches getList() zurückgibt, sind nicht unsere eigentlichen Daten. Das ist nicht möglich, da HTTP-Anfragen asynchron sind. Wenn ihr AJAX kennt, wisst ihr das bereits, schließlich steht das „A“ für asynchron. Was wir zurückerhalten ist ein sogenanntes Promise. Dies ist ein in AngularJS weit verbreitetes Konzept. Es gehört jedoch nicht spezifisch zu AngularJS und kann bspw. auch in Node eingesetzt werden. Ein Promise hilft uns asynchronen Code leserlicher zu schreiben. In diesem Fall ist then() ein Teil der Promise API. Der Callback, den then() erhält, wird ausgeführt, sobald der Promise erfüllt wurde. Anders formuliert: Der Callback von then() wird ausgeführt, wenn wir unsere Todos erhalten haben. Die Todos werden dem Callback übergeben, sodass wir sie $scope.todos-Array zuweisen können. Aber Achtung: Die Todos, die wir erhalten, sind nicht exakt unsere Todos. Wenn ihr sie mit console.log betrachtet, seht ihr, dass sie viel mehr Felder und auch Funktionen besitzen. Es handelt sich hierbei um von Restangular erweiterte Versionen unserer Todos! Dies ermöglicht uns später einen besseren Umgang mit den Daten.

1
2
3
4
5
6
7
          $scope.todos = [];
          Restangular
            .all('api/todos')
            .getList()
            .then(function(todos) {
              $scope.todos = todos;
            });

Da wir nun unsere Todos haben, wollen wir sie verändern können. Unser Client bietet die Möglichkeit neue Todos anzulegen und bestehende zu löschen. Aus diesem Grund erstellen wir in unserem Controller jeweils eine Funktion für diese Aufgabe: create und delete. Diese weisen wir ebenfalls dem $scope-Objekt zu, sodass sie später von der View aus aufgerufen werden können. In der create-Funktion nehmen wir unsere bestehenden Todos und fügen über post() ein neues Todo hinzu. (Denkt daran, dass unsere Todos von Restangular erweitert wurden. Die post()-Funktion ist eine solche Erweiterung, welche es uns nun erlaubt, schnell neue Todos zu erstellen.) Ihr seht ein neues Model namens newTodo auf dem $scope, welches wir bisher nicht erwähnt haben. Dabei handelt es sich um einen einzelnen String von einem Texteingabefeld, welches wir später in der View definieren. Von der post()-Funktion erhalten wir wieder ein Promise zurück, schließlich ist das Erstellen von neuen Daten über die REST API genauso asynchron wie das Abrufen bestehender Daten. Wurden unsere neue Todo erfolgreich übertragen, so wird der Promise erfüllt und der Callback von then() aufgerufen, in welchem wir unser neues Todo zum $scope hinzufügen. Dieser Schritt ist notwendig, damit AngularJS weiß, dass sich die Daten geändert haben. Die delete-Funktion ist sehr ähnlich aufgebaut, bezieht sich aber auf ein einzelnes Todo und nicht auf das gesamte Array. Die Funktion besitzt auch einen zweiten Parameter index, der jedoch nur dazu dient, dass gelöschte Todo im Array leichter zu finden.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
          $scope.create = function() {
            $scope.todos
              .post({ title: $scope.newTodo })
              .then(function(todo){
                $scope.todos.push(todo);
              });
          };

          $scope.delete = function(todo, index) {
            todo
              .remove()
              .then(function() {
                $scope.todos.splice(index, 1);
              });
          };

Es folgt ein kurzer CSS-Abschnitt, in welchem nur ein Stil festgehalten wurde. Elemente mit der Klasse completed sollen einen grünen Text erhalten. So können wir erledigte und nicht erledigte Todos unterscheiden.

Jetzt folgt nur noch unsere View. Die View sitzt in einem div-Element, dem wir unseren TodoController zuweisen. Auf diese Weise können wir die Daten und Funktionen, die der Controller über $scope offenlegt, in der View nutzen. Die View besteht aus zwei Teilen: einem Fomular zum Anlegen neuer Todos und einer Liste zum Anzeigen und Bearbeiten bestehender Todos. Das Formular besteht im Wesentlichen aus einem Textfeld, dessen Wert an das Model newTodo gebunden ist, und einem Button, der die create-Funktion des Controllers bei einem Click aufruft.
Die Liste sieht etwas komplizierter aus. Es wurde ein einzelnes li-Element in der Liste definiert, welches mit ng-repeat="todo in todos" beginnt. Es handelt sich hierbei um eine Direktive, welches das li-Element so oft wiederholt, wie es Einträge im Model todos gibt. Innerhalb von li kann ein einzelnes Todo über todo referenziert werden. Außerdem enthält das li-Element den Abschnitt ng-class="{ completed: todo.completed }". Dieser setzt automatisch die CSS-Klasse completed auf das li-Element, wenn das dazugehörige todo den Wert true im Feld completed besitzt! Ist dies nicht der Fall, wird die Klasse nicht gesetzt. Durch two-way data binding passiert diese Überprüfung jederzeit, wenn sich der Wert ändert! Im li-Element zeigen wir über den Platzhalter {{todo.title}} zunächst das eigentliche Todo an. Es folgt ein Button, welcher bei einem Click die delete-Funktion des Controllers aufruft. Ihr seht hier die Verwendung einer besonderen Variable namens $index. Sie wird von AngularJS automatisch generiert und beschreibt den Index von todo innerhalb des todos-Arrays. Es folgt eine Checkbox dessen Wert direkt an das Feld completed gebunden ist. Wird die Checkbox benutzt, so verändert sich der Wert von true auf false und umgekehrt. Mit der Direktive ng-change können wir über eine Änderung informiert werden, sodass wir sie mit todo.put() über unsere REST API speichern können.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    <div ng-controller="TodoController">
      <form>
        New Todo:
        <input type="text" ng-model="newTodo">
        <button ng-click="create()">Add</button>
      </form>

      <ul>
        <li ng-repeat="todo in todos" ng-class="{ completed: todo.completed }">
          {{todo.title}}
          <button ng-click="delete(todo, $index)">Remove</button> or mark as completed:
          <input type="checkbox" ng-model="todo.completed" ng-change="todo.put()">
        </li>
      </ul>
    </div>

Testet jetzt die App unter http://127.0.0.1:1337/index.html. Legt Todos an, löscht Todos, markiert sie als erledigt. Ihr seht, dass sich jede Aktion in der GUI widerspiegelt. Schaut zwischendurch auch immer mal wieder auf http://127.0.0.1:1337/api/todos und ihr seht, dass sich die Daten gleichermaßen in der REST API widerspiegeln. Öffnet zwischendurch einen anderen Browser und ihr seht auch dort den jeweils letzten Datenstand.

Unsere fertige Webapplikation

Damit endet meine Einführung in den MEAN Stack. Sie war gewiss unvollständig und lückenhaft, aber ihr habt in kürzester Zeit Erfahrungen in einer Reihe unterschiedlichster Technologien gewinnen können. Sie sollen euch als Inspiration und für ein grundlegendes Verständnis dienen. Es benötigt jedoch viel mehr Aufwand ein tieferes Verständnis für den MEAN Stack zu gewinnen. Wenn ihr weiterührende Informationen benötigt oder Fragen habt, schreibt einfach in den Kommentaren.

Ich hoffe, ich konnte euch mit meiner Artikelserie einen kleinen Überblick über den MEAN Stack verschaffen.

Viele Grüße,
Pipo :)

Autor: Pipo

...kommt ursprünglich aus der Designerecke, ist aber im Laufe seines Studiums in die Webentwicklung gestolpert. Kann sich seit dem nicht so richtig entscheiden wo er hingehört und wagt deswegen vielleicht die Flucht nach vorne in ein komplett neues Gebiet. Vielleicht Management? Niemand weiß es. Auch er nicht.

Kommentare (5)

  1. Hallo Pipo, danke nochmals für diese gut nachvollziehbare Einführung, als Anfänger habe ich viel Neues gelernt. Ich muß jetzt aber erstmal JavaScript lernen ;-)

  2. Hallo Pipo, wäre nicht ein Minibeispiel mit AngularUI als Abschluß sehr interessant? Man könnte z.B. mit dem Datepicker ein Zieldatum für ein ToDo eingeben. Das Zusammenspiel von AngularJS mit GUI Frameworks wie jQueryUI ist ja nicht gerade trivial, wegen des 2-Way Databinding usw. Ich werde mich als Übung mal daran versuchen, bin aber wohl noch nicht gut genug.

    • Hey grüß dich :)

       

      Ein Artikel über komplexe GUI mit AngularJS wäre auf jeden Fall spannend, aber im Augenblick fehlt mir dafür wieder etwas Zeit :( Ich würde aber wahrscheinlich nicht zu AngularUI-Bootstrap greifen, da AngularJS erst vor kurzem auf Version 1.2 und Bootstrap auf Version 3 aktualisiert wurde. Das waren ziemlich große Updates und AngularUI hängt da etwas hinterher. Außerdem verwendet AngularUI nicht immer best practices, was ich ziemlich schade finde für so ein großes Projekt. So haben die Direktiven beispielsweise (noch) keinen eigenen Prefix – daraus können Probleme entstehen.

      Ein anderes spannendes AngularJS-Thema welches ich noch nicht behandelt habe, sind Router bzw. der ui-Router.

      Ein weiteres interessantes Thema, welches nicht mit Angular zu tun hat und es zeitlich auch nicht mehr in die Serie geschafft hat, wäre GruntJS. Mit GruntJS kann man bspw. viele kleine JavaScript-Dateien zu einer großen zusammenfassen. Um ehrlich zu sein… es gibt einige Dinge, die ich nicht mehr abdecken konnte. LessKarma… Vielleicht kommt mal eine Nachfolge-Serie :)

  3. Hallo,
    Danke für das tolle Tutorial. Zumindest bei mir liegt nach der Installation  lodash in 
    <script src=“./bower_components/lodash/dist/lodash.js“></script>
    ansatt in
    <script src=“./bower_components/lodash/lodash.js“></script>
    Nach der Änderung funktioniert auch wieder das Beispiel.
    Grüße
    Michael